C ++ 0x non ha semafori? Come sincronizzare i thread?


135

È vero che C ++ 0x arriverà senza semafori? Esistono già alcune domande su Stack Overflow per quanto riguarda l'uso dei semafori. Li uso sempre (semafori posix) per lasciare che un thread attenda qualche evento in un altro thread:

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

  event1.wait();

  ...
}

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

  event1.post();

  ...
}

Se lo farei con un mutex:

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

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

  ...
}

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

  doSomethingth1();

  event1.unlock();

  ...
}

Problema: è brutto e non è garantito che thread1 blocchi prima il mutex (dato che lo stesso thread dovrebbe bloccare e sbloccare un mutex, non puoi nemmeno bloccare event1 prima dell'inizio di thread0 e thread1).

Quindi, dal momento che boost non ha neanche i semafori, qual è il modo più semplice per ottenere quanto sopra?


Forse usi la condizione mutex e std :: promise e std :: future?
Yves,

Risposte:


179

Puoi facilmente crearne uno da un mutex e una variabile di condizione:

#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
qualcuno dovrebbe presentare una proposta al

7
un commento qui che inizialmente mi ha lasciato perplesso è il blocco in attesa, ci si potrebbe chiedere come può un thread superare la notifica se il blocco è trattenuto in attesa? la risposta un po 'scarsamente documentata è che condition_variable.wait pulsa il lucchetto, consentendo a un altro thread di superare le notifiche in modo atomico, almeno è così che lo capisco

31
È stato deliberatamente escluso da Boost sulla base del fatto che un semaforo è troppa corda con cui i programmatori possono impiccarsi. Le variabili di condizione presumibilmente sono più gestibili. Vedo il loro punto ma mi sento un po 'patrocinato. Presumo che la stessa logica si applichi a C ++ 11: i programmatori dovrebbero scrivere i loro programmi in modo che "naturalmente" utilizzi condvar o altre tecniche di sincronizzazione approvate. Fornire un semaforo andrebbe contro quello indipendentemente dal fatto che sia implementato sopra condvar o nativamente.
Steve Jessop,

5
Nota: vedi en.wikipedia.org/wiki/Spurious_wakeup per la logica alla base del while(!count_)ciclo.
Dan Nissenbaum,

3
@Maxim mi dispiace, non penso che tu abbia ragione. sem_wait e sem_post scendono anche solo su contesa (controlla sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c ) quindi il codice finisce per duplicare l'implementazione di libc, con potenziali bug. Se si intende la portabilità su qualsiasi sistema, potrebbe essere una soluzione, ma se è necessaria solo la compatibilità Posix, utilizzare il semaforo Posix.
xryl669,

107

Basato su risposta di Maxim Yegorushkin , ho cercato di fare l'esempio in stile C ++ 11.

#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
Puoi fare aspettare () anche un tre-liner:cv.wait(lck, [this]() { return count > 0; });
Domi il

2
Aggiungere anche un'altra classe nello spirito di lock_guard è utile. In modo RAII, il costruttore, che prende come riferimento il semaforo, chiama la chiamata wait () del semaforo e il distruttore chiama la sua chiamata notification (). Ciò impedisce alle eccezioni di non riuscire a rilasciare il semaforo.
Jim Hunziker,

non c'è un dead-lock, se dicono N thread chiamati wait () e count == 0, allora cv.notify_one (); non viene mai chiamato, dal momento che il mtx non è stato rilasciato?
Marcello,

1
@Marcello I thread in attesa non tengono il blocco. L'intero punto delle variabili di condizione è fornire un'operazione atomica di "sblocco e attesa".
David Schwartz,

3
Dovresti rilasciare il blocco prima di chiamare notification_one () per evitare di bloccare immediatamente il wakeup ... vedi qui: en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

Ho deciso di scrivere il semaforo C ++ 11 più robusto / generico che potessi, nello stile dello standard il più possibile (nota using semaphore = ..., normalmente useresti semplicemente il nome semaphoresimile al normale usando stringnot 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();
}

Funziona, con una modifica minore. Il metodo wait_fore wait_untilchiama con il predicato restituisce un valore booleano (non un `std :: cv_status).
jdknight

mi dispiace per il nit-pick così tardi nel gioco. std::size_tè senza segno, quindi decrementando sotto lo zero è UB e lo sarà sempre >= 0. IMHO countdovrebbe essere un int.
Richard Hodges,

3
@RichardHodges non c'è modo di decrementare sotto lo zero, quindi non ci sono problemi, e cosa significherebbe un conteggio negativo su un semaforo? Non ha nemmeno senso IMO.
David,

1
@David E se un thread dovesse aspettare che gli altri inizino a inizializzare le cose? per esempio, 1 thread del lettore in attesa di 4 thread, chiamerei il costruttore del semaforo con -3 per far attendere il thread del lettore fino a quando tutti gli altri thread non hanno scritto un post. Immagino che ci siano altri modi per farlo, ma non è ragionevole? Penso che sia in effetti la domanda che l'OP sta ponendo, ma con più "thread1".
Jmmut,

2
@RichardHodges è molto pedante, decrementando un tipo intero senza segno inferiore a 0 non è UB.
jcai,

15

in accordo con i semafori posix, aggiungerei

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

E preferisco di gran lunga usare un meccanismo di sincronizzazione a un livello di astrazione conveniente, piuttosto che copiare sempre incollando una versione cucita insieme usando più operatori di base.


9

Puoi anche dare un'occhiata a cpp11-on-multicore - ha un'implementazione semaforo portatile e ottimale.

Il repository contiene anche altri gadget di threading che completano il threading c ++ 11.


8

Puoi lavorare con le variabili mutex e condition. Ottieni l'accesso esclusivo con il mutex, controlla se vuoi continuare o devi aspettare l'altra estremità. Se devi aspettare, aspetti in una condizione. Quando l'altro thread determina che è possibile continuare, segnala la condizione.

C'è un breve esempio nella libreria boost :: thread che molto probabilmente puoi semplicemente copiare (le librerie C ++ 0x e boost thread sono molto simili).


Le condizioni segnalano solo i thread in attesa o no? Quindi se thread0 non è in attesa quando thread1 segnala che verrà bloccato in seguito? Inoltre: non ho bisogno del blocco aggiuntivo fornito con la condizione - è sovraccarico.
Tauran,

Sì, la condizione segnala solo i thread in attesa. Il modello comune sta avendo una variabile con lo stato e una condizione nel caso in cui sia necessario attendere. Pensa a un produttore / consumatore, ci sarà un conteggio degli articoli nel buffer, il produttore si blocca, aggiunge l'elemento, incrementa il conteggio e segnali. Il consumatore si blocca, controlla il contatore e se non zero consuma, mentre se lo zero attende nella condizione.
David Rodríguez - dribeas,

2
Puoi simulare un semaforo in questo modo: inizializza una variabile con il valore che assegneresti al semaforo, quindi wait()viene tradotto in "blocco, controlla il conteggio se decrementa diverso da zero e continua; se zero attendi sulla condizione" mentre postsarebbe "blocca, contatore di incrementi, segnala se fosse 0 "
David Rodríguez - dribeas

Sì, suona bene. Mi chiedo se i semafori posix siano implementati allo stesso modo.
Tauran,

@tauran: Non lo so per certo (e potrebbe dipendere da quale sistema operativo Posix), ma penso sia improbabile. I semafori sono tradizionalmente una primitiva di sincronizzazione "di livello inferiore" rispetto ai mutex e alle variabili di condizione, e in linea di principio possono essere resi più efficienti di quanto sarebbero se implementati su una condvar. Quindi, più probabilmente in un dato sistema operativo è che tutte le primitive di sincronizzazione a livello di utente sono costruite su alcuni strumenti comuni che interagiscono con lo scheduler.
Steve Jessop,

3

Inoltre può essere utile wrapper semaforo RAII nei thread:

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;
};

Esempio di utilizzo nell'app multithread:

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 avrà finalmente i semafori - std::counting_semaphore<max_count> .

Questi avranno (almeno) i seguenti metodi:

  • acquire() (Bloccaggio)
  • try_acquire() (non bloccante, restituisce immediatamente)
  • try_acquire_for() (non bloccante, richiede una durata)
  • try_acquire_until() (non bloccante, richiede del tempo per smettere di provare)
  • release()

Questo non è ancora elencato su cppreference, ma puoi leggere queste diapositive di presentazione di CppCon 2019 o guardare il video . C'è anche la proposta ufficiale P0514R4 , ma non sono sicuro che sia la versione più aggiornata.


2

Ho trovato shared_ptr e weak_ptr, un lungo con un elenco, fatto il lavoro di cui avevo bisogno. Il mio problema era che avevo diversi clienti che volevano interagire con i dati interni di un host. In genere, l'host aggiorna i dati da solo, tuttavia, se un client lo richiede, l'host deve interrompere l'aggiornamento fino a quando nessun client accede ai dati dell'host. Allo stesso tempo, un client potrebbe richiedere l'accesso esclusivo, in modo che nessun altro client, né l'host, possa modificare i dati dell'host.

Come ho fatto, ho creato una struttura:

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

Ogni cliente avrebbe un membro di tale:

UpdateLock::ptr m_myLock;

Quindi l'host avrebbe un membro weak_ptr per esclusività e un elenco di weak_ptrs per blocchi non esclusivi:

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

Esiste una funzione per abilitare il blocco e un'altra funzione per verificare se l'host è bloccato:

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

Testare i blocchi in LockUpdate, IsUpdateLocked e periodicamente nella routine di aggiornamento dell'host. Testare un blocco è semplice come controllare se il punto debole è scaduto e rimuovere eventuali scaduti dall'elenco m_locks (lo faccio solo durante l'aggiornamento dell'host), posso verificare se l'elenco è vuoto; allo stesso tempo, ottengo lo sblocco automatico quando un client reimposta il shared_ptr a cui si sta aggrappando, cosa che accade anche quando un client viene distrutto automaticamente.

L'effetto complessivo è che, poiché i client raramente necessitano di esclusività (in genere riservata solo a aggiunte ed eliminazioni), il più delle volte una richiesta a LockUpdate (false), vale a dire non esclusiva, ha successo fino a quando (! M_exclusiveLock). E un LockUpdate (true), una richiesta di esclusività, riesce solo quando entrambi (! M_exclusiveLock) e (m_locks.empty ()).

È possibile aggiungere una coda per mitigare i blocchi esclusivi e non esclusivi, tuttavia finora non ho avuto collisioni, quindi ho intenzione di aspettare fino a quando ciò accadrà per aggiungere la soluzione (principalmente quindi ho una condizione di test nel mondo reale).

Finora questo funziona bene per le mie esigenze; Posso immaginare la necessità di espandere questo, e alcuni problemi che potrebbero sorgere su un uso esteso, tuttavia, questo è stato veloce da implementare e ha richiesto pochissimo codice personalizzato.


-4

Nel caso in cui qualcuno sia interessato alla versione atomica, ecco l'implementazione. Le prestazioni sono previste migliori rispetto alla versione variabile mutex e condition.

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
Mi aspetterei che la performance peggiori molto . Questo codice fa quasi letteralmente ogni possibile errore. Come solo l'esempio più ovvio, supponiamo che il waitcodice debba essere ripetuto più volte. Quando alla fine si sbloccherà, prenderà la madre di tutti i rami mal previsti, poiché la previsione del ciclo della CPU prevederà sicuramente che si ripeterà. Potrei elencare molti altri problemi con questo codice.
David Schwartz,

1
Ecco un altro ovvio performance killer: il waitloop consumerà risorse di microesecuzione della CPU mentre gira. Supponiamo che sia nello stesso nucleo fisico del thread che dovrebbe notifysupporlo - rallenterà terribilmente quel thread.
David Schwartz,

1
Ed eccone un'altra: sulle CPU x86 (le CPU più popolari oggi), un'operazione compare_exchange_weak è sempre un'operazione di scrittura, anche se fallisce (riscrive lo stesso valore che legge se il confronto fallisce). Supponiamo quindi che due core siano entrambi in waitloop per lo stesso semaforo. Stanno entrambi scrivendo a tutta velocità sulla stessa linea di cache, che può rallentare la scansione di altri core saturando i bus inter-core.
David Schwartz,

@DavidSchwartz Lieto di vedere i tuoi commenti. Non sono sicuro di capire la parte "... previsione loop della CPU ...". Concordato il 2 °. Apparentemente il tuo terzo caso può accadere, ma confrontandolo con mutex che causa la commutazione della modalità utente alla modalità kernel e la chiamata di sistema, la sincronizzazione inter-core non è peggiore.
Jeffery,

1
Non esiste un semaforo senza lock. L'idea di essere free lock non è scrivere codice senza usare i mutex, ma scrivere codice dove un thread non si blocca mai affatto. In questo caso la vera essenza del semaforo è bloccare i thread che chiamano la funzione wait ()!
Carlo Wood,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.