146 votes

C++0x n'a pas de sémaphores ? Comment synchroniser les threads ?

Est-il vrai que le C++0x sera dépourvu de sémaphores ? Il y a déjà quelques questions sur Stack Overflow concernant l'utilisation des sémaphores. Je les utilise (sémaphores posix) tout le temps pour laisser un thread attendre un événement dans un autre thread :

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

  event1.wait();

  ...
}

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

  event1.post();

  ...
}

Si je faisais ça avec un mutex :

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

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

  ...
}

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

  doSomethingth1();

  event1.unlock();

  ...
}

Problème : c'est laid et il n'est pas garanti que le thread1 verrouille le mutex en premier (étant donné que le même thread doit verrouiller et déverrouiller un mutex, vous ne pouvez pas non plus verrouiller l'événement1 avant que le thread0 et le thread1 ne commencent).

Puisque boost n'a pas non plus de sémaphores, quel est le moyen le plus simple de réaliser ce qui précède ?

1 votes

Peut-être utiliser la condition mutex et std::promise et std::future ?

204voto

Maxim Yegorushkin Points 29380

Vous pouvez facilement en construire un à partir d'un mutex et d'une variable de condition :

#include <mutex>
#include <condition_variable>

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

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

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

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

104 votes

Quelqu'un devrait soumettre une proposition au comité des normes.

0 votes

Je ne suis pas sûr que cela justifierait une inclusion dans la norme. Premièrement, elle peut être mise en œuvre en utilisant les primitives de synchronisation existantes. Deuxièmement, elle peut être mise en œuvre de plusieurs façons.

9 votes

Un commentaire qui m'a laissé perplexe au départ est le verrou dans wait, on pourrait se demander comment un thread peut dépasser notify si le verrou est détenu par wait ? la réponse quelque peu obscure et documentée est que condition_variable.wait pulse le verrou, permettant à un autre thread de dépasser notify de façon atomique, du moins c'est comme ça que je le comprends

111voto

Tsuneo Yoshioka Points 702

Sur la base de La réponse de Maxim Yegorushkin J'ai essayé de faire l'exemple dans le style 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;
};

35 votes

Vous pouvez faire en sorte que wait() soit également une ligne de trois : cv.wait(lck, [this]() { return count > 0; });

2 votes

L'ajout d'une autre classe dans l'esprit de lock_guard est également utile. A la manière de RAII, le constructeur, qui prend le sémaphore comme référence, appelle l'appel wait() du sémaphore, et le destructeur appelle son appel notify(). Cela empêche les exceptions de ne pas libérer le sémaphore.

0 votes

N'y a-t-il pas un dead-lock, si disons N threads appellent wait() et count==0, alors cv.notify_one() ; n'est jamais appelé, puisque le mtx n'a pas été libéré ?

39voto

Dave Points 10916

J'ai décidé d'écrire le sémaphore C++11 le plus robuste/générique que je pouvais, en respectant autant que possible le style de la norme (remarque using semaphore = ... vous devez normalement utiliser le nom semaphore comme si vous utilisiez normalement string pas 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();
}

0 votes

Cela fonctionne, avec une modification mineure. Le site wait_for y wait_until Les appels à la méthode avec le prédicat retournent une valeur booléenne (et non un `std::cv_status).

1 votes

Désolé de pinailler si tard dans la partie. std::size_t est non signé, donc le décrémenter en dessous de zéro est UB, et il sera toujours >= 0 . IMHO count devrait être un int .

3 votes

@RichardHodges il n'y a aucun moyen de décrémenter en dessous de zéro donc il n'y a pas de problème, et que signifierait un compte négatif sur un sémaphore ? Cela n'a même pas de sens IMO.

15voto

Michael Zillich Points 71

En accord avec les sémaphores posix, j'ajouterais

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

Et je préfère de loin utiliser un mécanisme de synchronisation à un niveau d'abstraction pratique, plutôt que de toujours copier-coller une version cousue ensemble en utilisant des opérateurs plus basiques.

11voto

einpoklum Points 2893

Le C++20 dispose enfin de sémaphores - std::counting_semaphore<max_count> .

Ceux-ci disposent (au moins) des méthodes suivantes :

  • acquire() (blocage)
  • try_acquire() (non bloquant, retourne immédiatement)
  • try_acquire_for() (non bloquant, prend une durée)
  • try_acquire_until() (non bloquant, il faut un temps pour arrêter d'essayer)
  • release()

Vous pouvez lire ces diapositives de présentation de la CppCon 2019 ou regardez le vidéo . Il y a aussi la proposition officielle P0514R4 mais il se peut qu'il ne soit pas à jour par rapport à la version actuelle de C++20.

0 votes

Le C++20 n'a finalement pas de sémaphores...

2 votes

@Sandburg : A ma connaissance, c'est le cas.

0 votes

OK, ouais. include <semaphore> difficile à trouver... beaucoup de bruit de "boost" sur ce sujet.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X