C ++ 0x semafor yok mu? Konuları nasıl senkronize edebilirim?


135

C ++ 0x semaforlar olmadan gelecek doğru mu? Semafor kullanımıyla ilgili olarak Stack Overflow ile ilgili bazı sorular var. Onları (posix semaforları) her zaman bir iş parçacığı başka bir iş parçacığında bazı olay için beklemek izin kullanın:

void thread0(...)
{
  doSomething0();

  event1.wait();

  ...
}

void thread1(...)
{
  doSomething1();

  event1.post();

  ...
}

Bunu bir muteks ile yaparsam:

void thread0(...)
{
  doSomething0();

  event1.lock(); event1.unlock();

  ...
}

void thread1(...)
{
  event1.lock();

  doSomethingth1();

  event1.unlock();

  ...
}

Sorun: Çirkin ve thread1'in önce muteksi kilitlediği garanti edilmez (Aynı iş parçacığının muteksi kilitlemesi ve kilidini açması gerektiği göz önüne alındığında, event1'i thread0 ve thread1 başlamadan önce kilitleyemezsiniz).

Dolayısıyla, takviye semaforları da olmadığından, yukarıdakilere ulaşmanın en basit yolu nedir?


Belki mutex ve std :: promise ve std :: future koşullarını kullanın?
Yves

Yanıtlar:


179

Mutex ve koşul değişkeninden kolayca bir tane oluşturabilirsiniz:

#include <mutex>
#include <condition_variable>

class semaphore
{
private:
    std::mutex mutex_;
    std::condition_variable condition_;
    unsigned long count_ = 0; // Initialized as locked.

public:
    void notify() {
        std::lock_guard<decltype(mutex_)> lock(mutex_);
        ++count_;
        condition_.notify_one();
    }

    void wait() {
        std::unique_lock<decltype(mutex_)> lock(mutex_);
        while(!count_) // Handle spurious wake-ups.
            condition_.wait(lock);
        --count_;
    }

    bool try_wait() {
        std::lock_guard<decltype(mutex_)> lock(mutex_);
        if(count_) {
            --count_;
            return true;
        }
        return false;
    }
};

96
birisi standartlar komitesine bir teklif sunmalıdır

7
Burada ilk başta beni şaşırtan bir yorum, kilit beklemede, bir kilit bekle tutulursa bir iş parçacığı nasıl geçmiş olsun bildirebilirsiniz sorabilirsiniz? biraz kötü belgelenmiş bir cevap bu condition_variable.wait başka bir iş parçacığı atomik bir şekilde bildirmek için geçmiş izin veren, darbe darbeler, en azından ben böyle anlıyorum

31
Bu edilmiş kasıtlı bir semafor ile kendilerini asmak için programcılar için çok fazla ip olduğunu temelinde Boost hariç. Durum değişkenlerinin daha yönetilebilir olduğu varsayılmaktadır. Onların amacını görüyorum ama biraz kendimi azılı hissediyorum. Aynı mantığın C ++ 11 için de geçerli olduğunu varsayıyorum - programcıların programlarını "doğal olarak" kovarları veya diğer onaylanmış senkronizasyon tekniklerini kullanacak şekilde yazmaları bekleniyor. Bir semaforun, kondvarın üstüne mi yoksa doğal olarak mı uygulandığına bakılmaksızın, buna karşı koşacaktı.
Steve Jessop

5
Not - Döngünün arkasındaki mantık için bkz. En.wikipedia.org/wiki/Spurious_wakeupwhile(!count_) .
Dan Nissenbaum

3
@Maxim Üzgünüm, haklı olduğunu düşünmüyorum. sem_wait ve sem_post yalnızca çekişme konusunda sistematik olarak arama yapar (check sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c ), bu nedenle kod, libc uygulamasını potansiyel hatalarla çoğaltır. Herhangi bir sistemde taşınabilirliği planlıyorsanız, bu bir çözüm olabilir, ancak yalnızca Posix uyumluluğuna ihtiyacınız varsa Posix semaforunu kullanın.
xryl669

107

Maxim Yegorushkin'in cevabına dayanarak , örneği C ++ 11 tarzında yapmaya çalıştım.

#include <mutex>
#include <condition_variable>

class Semaphore {
public:
    Semaphore (int count_ = 0)
        : count(count_) {}

    inline void notify()
    {
        std::unique_lock<std::mutex> lock(mtx);
        count++;
        cv.notify_one();
    }

    inline void wait()
    {
        std::unique_lock<std::mutex> lock(mtx);

        while(count == 0){
            cv.wait(lock);
        }
        count--;
    }

private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;
};

34
Ayrıca beklemek () de üç astarlı yapabilirsiniz:cv.wait(lck, [this]() { return count > 0; });
Domi

2
Lock_guard ruhuna başka bir sınıf eklemek de faydalıdır. RAII tarzında, semaforu referans olarak alan yapıcı semaforun wait () çağrısını çağırır ve yıkıcı onun notify () çağrısını çağırır. Bu, istisnaların semaforu serbest bırakmamasını önler.
Jim Hunziker

ölü kilit yoksa, wait () ve count == 0 olarak adlandırılan N iş parçacıkları varsa, cv.notify_one (); mtx piyasaya sürülmediği için asla çağrılmıyor mu?
Marcello

1
@Marcello Bekleyen iş parçacıkları kilidi tutmaz. Koşul değişkenlerinin tamamı atomik bir "kilit açma ve bekleme" işlemi sağlamaktır.
David Schwartz

3
Uyanmayı hemen engellememek için notify_one () öğesini çağırmadan önce kilidi serbest bırakmalısınız ... buraya bakın: en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

Ben en sağlam / jenerik C ++ ı (not geldiğince 11 kadar standart tarzında, olabilir SEMAPHORE yazmaya karar using semaphore = ..., normalde sadece adı kullanır semaphorebenzer normalde kullanarak stringdeğil basic_string):

template <typename Mutex, typename CondVar>
class basic_semaphore {
public:
    using native_handle_type = typename CondVar::native_handle_type;

    explicit basic_semaphore(size_t count = 0);
    basic_semaphore(const basic_semaphore&) = delete;
    basic_semaphore(basic_semaphore&&) = delete;
    basic_semaphore& operator=(const basic_semaphore&) = delete;
    basic_semaphore& operator=(basic_semaphore&&) = delete;

    void notify();
    void wait();
    bool try_wait();
    template<class Rep, class Period>
    bool wait_for(const std::chrono::duration<Rep, Period>& d);
    template<class Clock, class Duration>
    bool wait_until(const std::chrono::time_point<Clock, Duration>& t);

    native_handle_type native_handle();

private:
    Mutex   mMutex;
    CondVar mCv;
    size_t  mCount;
};

using semaphore = basic_semaphore<std::mutex, std::condition_variable>;

template <typename Mutex, typename CondVar>
basic_semaphore<Mutex, CondVar>::basic_semaphore(size_t count)
    : mCount{count}
{}

template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::notify() {
    std::lock_guard<Mutex> lock{mMutex};
    ++mCount;
    mCv.notify_one();
}

template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::wait() {
    std::unique_lock<Mutex> lock{mMutex};
    mCv.wait(lock, [&]{ return mCount > 0; });
    --mCount;
}

template <typename Mutex, typename CondVar>
bool basic_semaphore<Mutex, CondVar>::try_wait() {
    std::lock_guard<Mutex> lock{mMutex};

    if (mCount > 0) {
        --mCount;
        return true;
    }

    return false;
}

template <typename Mutex, typename CondVar>
template<class Rep, class Period>
bool basic_semaphore<Mutex, CondVar>::wait_for(const std::chrono::duration<Rep, Period>& d) {
    std::unique_lock<Mutex> lock{mMutex};
    auto finished = mCv.wait_for(lock, d, [&]{ return mCount > 0; });

    if (finished)
        --mCount;

    return finished;
}

template <typename Mutex, typename CondVar>
template<class Clock, class Duration>
bool basic_semaphore<Mutex, CondVar>::wait_until(const std::chrono::time_point<Clock, Duration>& t) {
    std::unique_lock<Mutex> lock{mMutex};
    auto finished = mCv.wait_until(lock, t, [&]{ return mCount > 0; });

    if (finished)
        --mCount;

    return finished;
}

template <typename Mutex, typename CondVar>
typename basic_semaphore<Mutex, CondVar>::native_handle_type basic_semaphore<Mutex, CondVar>::native_handle() {
    return mCv.native_handle();
}

Bu, küçük bir düzenleme ile çalışır. wait_forVe wait_untilyüklemi yöntem çağrıları bir Boole değeri (bir 'Std :: cv_status) döndürür.
jdknight

oyunda bu kadar geç nit-pick için üzgünüm. std::size_timzasız olduğundan sıfırın altına inmek UB'dir ve her zaman olacaktır >= 0. IMHO bir countolmalıdır int.
Richard Hodges

3
@RichardHodges Sıfırın altına inmenin bir yolu yoktur, bu yüzden sorun yoktur ve semafordaki negatif sayım ne anlama gelir? Bu IMO bile mantıklı değil.
David

1
@David Bir iş parçacığının başkalarının şeyleri icat etmesini beklemesi gerekiyorsa ne olur? Örneğin, 4 iş parçacığı beklemek için 1 okuyucu iplik, ben diğer iş parçacığı bir yazı yapılan kadar okuyucu iplik beklemek yapmak için semafor yapıcı -3 ile çağırır. Sanırım bunu yapmanın başka yolları da var, ama mantıklı değil mi? Aslında OP soruyor ama daha fazla "thread1" s soru olduğunu düşünüyorum.
20mm

2
@RichardHodges çok bilgiç olmak, işaretsiz tam sayı türünü 0'ın altına düşürmek UB değildir.
jcai

15

posix semaforlarına uygun olarak,

class semaphore
{
    ...
    bool trywait()
    {
        boost::mutex::scoped_lock lock(mutex_);
        if(count_)
        {
            --count_;
            return true;
        }
        else
        {
            return false;
        }
    }
};

Ve daha temel operatörleri kullanarak her zaman dikişli bir versiyonu kopyalayıp yapıştırmak yerine, uygun bir soyutlama seviyesinde bir senkronizasyon mekanizması kullanmayı tercih ederim.


9

Ayrıca çok çekirdekli cpp11'i kontrol edebilirsiniz - taşınabilir ve en uygun semafor uygulaması vardır.

Depo ayrıca c ++ 11 iş parçacığını tamamlayan diğer iş parçacığı güzelliklerini de içerir.


8

Muteks ve koşul değişkenleriyle çalışabilirsiniz. Muteks ile özel erişim elde edersiniz, devam etmek isteyip istemediğinizi veya diğer ucu beklemek zorunda olup olmadığınızı kontrol edin. Beklemeniz gerekiyorsa, bir koşulda beklersiniz. Diğer iş parçacığı devam edebileceğinizi belirlediğinde, durumu işaret eder.

Boost :: thread kütüphanesinde büyük olasılıkla sadece kopyalayabileceğiniz kısa bir örnek vardır (C ++ 0x ve boost thread kütüphaneleri çok benzerdir).


Durum sadece bekleyen iş parçacıklarına sinyal veriyor, değil mi? Peki thread0 orada thread1 sinyalleri beklerken yoksa daha sonra bloke olur? Artı: Durumla birlikte gelen ek kilide ihtiyacım yok - bu havai.
tauran

Evet, koşul yalnızca bekleyen iş parçacıklarını gösterir. Ortak desen, beklemeniz gerektiğinde durum ve koşul ile bir değişkene sahip olmaktır. Bir üretici / tüketici üzerinde düşünün, tampondaki öğeler üzerinde bir sayı olacaktır, üretici kilitler, elemanı ekler, sayımı ve sinyalleri arttırır. Tüketici kilitler, sayacı kontrol eder ve sıfırdan fazla tüketirse, sıfır durumda kalırsa.
David Rodríguez - dribeas

2
Bir semaforu şu şekilde simüle edebilirsiniz: Semaforu vereceğiniz değere sahip bir değişkeni başlatın, sonra wait()"kilitle, sıfır olmayan bir azalma durumunda sayımı kontrol edin ve devam edin; koşulda sıfır beklerse post", artış sayacı, 0 ise sinyal "
David Rodríguez - dribeas

Evet! kulağa hoş geliyor. Posix semaforlarının aynı şekilde uygulanıp uygulanmadığını merak ediyorum.
tauran

@taurant: Emin değilim (ve hangi Posix OS bağlı olabilir), ama ben olası düşünüyorum. Semaforlar geleneksel olarak mutekslerden ve koşul değişkenlerinden daha "düşük seviyeli" bir senkronizasyondur ve prensip olarak bir kovarın üzerine uygulandığında olduğundan daha verimli hale getirilebilir. Bu nedenle, belirli bir işletim sisteminde daha büyük olasılıkla, tüm kullanıcı düzeyi eşzamanlama ilkeleri, zamanlayıcı ile etkileşime giren bazı yaygın araçların üzerine inşa edilir.
Steve Jessop

3

Ayrıca ipliklerde yararlı RAII semafor sarıcı olabilir:

class ScopedSemaphore
{
public:
    explicit ScopedSemaphore(Semaphore& sem) : m_Semaphore(sem) { m_Semaphore.Wait(); }
    ScopedSemaphore(const ScopedSemaphore&) = delete;
    ~ScopedSemaphore() { m_Semaphore.Notify(); }

   ScopedSemaphore& operator=(const ScopedSemaphore&) = delete;

private:
    Semaphore& m_Semaphore;
};

Çok iş parçacıklı uygulamada kullanım örneği:

boost::ptr_vector<std::thread> threads;
Semaphore semaphore;

for (...)
{
    ...
    auto t = new std::thread([..., &semaphore]
    {
        ScopedSemaphore scopedSemaphore(semaphore);
        ...
    }
    );
    threads.push_back(t);
}

for (auto& t : threads)
    t.join();

3

C ++ 20 nihayet semaforlara sahip olacak - std::counting_semaphore<max_count>.

Bunlar (en azından) aşağıdaki yöntemlere sahip olacaktır:

  • acquire() (Engelleme)
  • try_acquire() (tıkanmaz, anında geri döner)
  • try_acquire_for() (engellemez, bir süre alır)
  • try_acquire_until() (engellemez, denemeyi durdurmak için zaman alır)
  • release()

Bu henüz cppreference'de listelenmemiştir, ancak bu CppCon 2019 sunum slaytlarını okuyabilir veya videoyu izleyebilirsiniz . Ayrıca resmi teklif P0514R4 var , ama bunun en güncel sürüm olduğundan emin değilim.


2

Ihtiyacım olan işi bir liste ile uzun, shared_ptr ve poor_ptr buldum. Benim sorunum, bir ana bilgisayarın dahili verileriyle etkileşim kurmak isteyen birkaç müşterim vardı. Genellikle, ana bilgisayar verileri kendi başına güncelleştirir, ancak bir istemci isterse, ana bilgisayar verilerine erişene kadar ana bilgisayarın güncelleştirmeyi durdurması gerekir. Aynı zamanda, bir istemci özel erişim isteyebilir, böylece başka hiçbir istemci veya ana bilgisayar bu ana bilgisayar verilerini değiştiremez.

Bunu nasıl yaptım, bir yapı oluşturdum:

struct UpdateLock
{
    typedef std::shared_ptr< UpdateLock > ptr;
};

Her müşterinin böyle bir üyesi olacaktır:

UpdateLock::ptr m_myLock;

Daha sonra ana bilgisayarda münhasırlık için bir zayıf_ptr üyesi ve münhasır olmayan kilitler için bir zayıf_ptr listesi bulunur:

std::weak_ptr< UpdateLock > m_exclusiveLock;
std::list< std::weak_ptr< UpdateLock > > m_locks;

Kilidi etkinleştirmek için bir işlev ve ana bilgisayarın kilitli olup olmadığını kontrol etmek için başka bir işlev vardır:

UpdateLock::ptr LockUpdate( bool exclusive );       
bool IsUpdateLocked( bool exclusive ) const;

LockUpdate, IsUpdateLocked ve periyodik olarak ana bilgisayarın Update yordamındaki kilitleri test ediyorum. Bir kilidi sınamak için shor_ptr süresinin dolup dolmadığını kontrol etmek ve m_locks listesinden süresi dolmuş herhangi bir şeyi kaldırmak kadar basittir (bunu sadece ana bilgisayar güncellemesi sırasında yaparım), listenin boş olup olmadığını kontrol edebilirim; Aynı zamanda, bir istemci asılı kaldıkları paylaşılan_ptr'i sıfırladığında otomatik kilidini açıyorum, bu da bir istemci otomatik olarak yok edildiğinde gerçekleşir.

Hepsinden önemlisi, müşteriler nadiren münhasırlığa ihtiyaç duyduklarından (genellikle yalnızca eklemeler ve silme işlemleri için ayrılmıştır), çoğu zaman LockUpdate (yanlış), yani münhasır olmayan bir istek (! M_exclusiveLock) kadar başarılı olur. Ve bir LockUpdate (true), münhasırlık isteği, sadece (! M_exclusiveLock) ve (m_locks.empty ()) hem de başarılı olur.

Özel ve münhasır olmayan kilitler arasında hafifletmek için bir kuyruk eklenebilir, ancak şimdiye kadar herhangi bir çarpışma yaşamadım, bu yüzden çözümü eklemek için bekleyin (çoğunlukla gerçek dünya test koşulum var).

Şimdiye kadar bu benim ihtiyaçlarım için iyi çalışıyor; Bunu genişletme ihtiyacını ve genişletilmiş kullanımda ortaya çıkabilecek bazı sorunları hayal edebiliyorum, ancak bu uygulanması hızlıydı ve çok az özel kod gerektiriyordu.


-4

Birisinin atom versiyonuyla ilgilenmesi durumunda, uygulama burada. Performansın mutex & condition değişken versiyonundan daha iyi olması bekleniyor.

class semaphore_atomic
{
public:
    void notify() {
        count_.fetch_add(1, std::memory_order_release);
    }

    void wait() {
        while (true) {
            int count = count_.load(std::memory_order_relaxed);
            if (count > 0) {
                if (count_.compare_exchange_weak(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
                    break;
                }
            }
        }
    }

    bool try_wait() {
        int count = count_.load(std::memory_order_relaxed);
        if (count > 0) {
            if (count_.compare_exchange_strong(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
                return true;
            }
        }
        return false;
    }
private:
    std::atomic_int count_{0};
};

4
Performansın çok daha kötü olmasını beklerdim . Bu kod, kelimenin tam anlamıyla olası her hatayı yapar. En açık örnek olarak, waitkodun birkaç kez döngü yapması gerektiğini varsayalım . Sonunda engelini kaldırdığında, CPU'nun döngü tahmini kesinlikle tekrar döngü yapacağını tahmin edeceğinden, tüm yanlış tahmin edilen dalların annesini alacaktır. Bu kodla ilgili daha birçok sorunu listeleyebilirim.
David Schwartz

1
İşte bir başka belirgin performans katili: waitDöngü, CPU mikro-yürütme kaynaklarını dönerken tüketecek. Farzedilen iplikle aynı fiziksel çekirdeğe sahip olduğunu varsayalım notify- bu ipliği çok yavaşlatır.
David Schwartz

1
Ve işte sadece bir tane daha: x86 CPU'larda (bugünün en popüler CPU'ları), Compare_exchange_weak işlemi başarısız olsa bile her zaman bir yazma işlemidir (karşılaştırma başarısız olursa okuduğu değeri geri yazar). Diyelim ki iki çekirdek waitaynı semafor için bir döngü içinde . Her ikisi de tam hızda aynı önbellek satırına yazıyor , bu da çekirdekler arası otobüsleri doyurarak diğer çekirdekleri taramayı yavaşlatabiliyor.
David Schwartz

@DavidSchwartz Yorumlarınızı gördüğünüze sevindim. '... CPU'nun döngü tahmini ...' bölümünü anladığınızdan emin değilim. İkinci olanı kabul etti. Görünüşe göre 3. durumunuz olabilir, ancak kullanıcı modunun çekirdek modu anahtarına ve sistem çağrısına neden olan mutekse kıyasla, çekirdekler arası senkronizasyon daha kötü değildir.
Jeffery

1
Kilitsiz semafor diye bir şey yoktur. Kilitsiz olmanın tüm fikri muteksleri kullanmadan kod yazmak değil, bir iş parçacığının hiçbir zaman engellemediği bir kod yazmaktır. Bu durumda semaforun özü wait () işlevini çağıran evreleri engellemektir!
Carlo Wood
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.