Est-il vrai que C++ 0x viendra sans sémaphores? Il y a déjà quelques questions sur le dépassement de pile 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 le ferais avec un mutex:
void thread0(...)
{
doSomething0();
event1.lock(); event1.unlock();
...
}
void thread1(...)
{
event1.lock();
doSomethingth1();
event1.unlock();
...
}
Problème: c’est moche et il n’est pas garanti que thread1 verrouille d’abord le mutex (étant donné que le même thread doit verrouiller et déverrouiller un mutex, vous ne pouvez pas non plus verrouiller event1 avant que thread0 et thread1 ne soient démarrés).
Donc, puisque boost n’a pas non plus de sémaphores, quel est le moyen le plus simple d’atteindre ces objectifs?
Vous pouvez facilement en construire un à partir d'un mutex et d'une variable de condition:
#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;
}
};
Basé sur réponse de Maxim Yegorushkin , j'ai essayé de créer 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;
};
J'ai décidé d'écrire le sémaphore C++ 11 le plus robuste/générique possible, dans le style de la norme autant que je pouvais (note using semaphore = ...
, vous utiliseriez normalement le nom semaphore
comme 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();
}
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 convenable, plutôt que de toujours copier-coller une version assemblée à l'aide d'opérateurs plus basiques.
Vous pouvez également vérifier cpp11-on-multicore - il possède une implémentation de sémaphore portable et optimale.
Le référentiel contient également d'autres goodies de threading qui complètent le threading c ++ 11.
Vous pouvez travailler avec des variables mutex et de condition. Vous obtenez un accès exclusif avec le mutex, vérifiez si vous souhaitez continuer ou devez attendre l'autre extrémité. Si vous devez attendre, vous attendez dans un état. Lorsque l'autre thread détermine que vous pouvez continuer, il signale la situation.
Il y a un court exemple dans la bibliothèque boost :: thread que vous pouvez probablement simplement copier (les bibliothèques de threads C++ 0x et boost sont très similaires).
Peut également être utile wrapper de sémaphore RAII dans les threads:
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;
};
Exemple d'utilisation dans une application 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();
J'ai trouvé le shared_ptr et faible_ptr, un long avec une liste, fait le travail dont j'avais besoin. Mon problème était que plusieurs clients souhaitaient interagir avec les données internes d'un hôte. En règle générale, l'hôte met à jour les données lui-même. Toutefois, si un client le demande, l'hôte doit cesser de mettre à jour jusqu'à ce qu'aucun client n'ait accès aux données de l'hôte. Dans le même temps, un client peut demander un accès exclusif, afin qu'aucun autre client, ni l'hôte, ne puisse modifier ces données.
Comment j'ai fait cela, j'ai créé une structure:
struct UpdateLock
{
typedef std::shared_ptr< UpdateLock > ptr;
};
Chaque client aurait un membre de tel:
UpdateLock::ptr m_myLock;
Ensuite, l'hôte aurait un membre faible_ptr pour l'exclusivité et une liste de faibles_ptrs pour les verrous non exclusifs:
std::weak_ptr< UpdateLock > m_exclusiveLock;
std::list< std::weak_ptr< UpdateLock > > m_locks;
Il existe une fonction pour activer le verrouillage et une autre pour vérifier si l'hôte est verrouillé:
UpdateLock::ptr LockUpdate( bool exclusive );
bool IsUpdateLocked( bool exclusive ) const;
Je teste les verrous dans LockUpdate, IsUpdateLocked et périodiquement dans la routine de mise à jour de l'hôte. Tester un verrou est aussi simple que de vérifier si le fichier faibles_ptr a expiré et de supprimer ceux qui ont expiré de la liste m_locks (je ne le fais que lors de la mise à jour de l'hôte), je peux vérifier si la liste est vide; en même temps, je reçois un déverrouillage automatique lorsqu'un client réinitialise le shared_ptr auquel il est accroché, ce qui se produit également lorsqu'un client est automatiquement détruit.
L'effet global est le suivant: puisque les clients ont rarement besoin de l'exclusivité (généralement réservée aux ajouts et aux suppressions), la plupart du temps une demande à LockUpdate (false), c'est-à-dire non exclusive, réussit aussi longtemps que (! M_exclusiveLock). Et une LockUpdate (true), une demande d'exclusivité, ne réussit que lorsque (! M_exclusiveLock) et (m_locks.empty ()).
Une file d'attente pourrait être ajoutée pour atténuer les verrouillages exclusif et non exclusif. Toutefois, je n'ai eu aucune collision jusqu'à présent. J'ai donc l'intention d'attendre que cela se produise pour ajouter la solution (la plupart du temps, j'ai donc des conditions de test réelles).
Jusqu'ici cela fonctionne bien pour mes besoins; Je peux imaginer la nécessité d’élargir cela, ainsi que certains problèmes pouvant survenir lors de l’utilisation élargie, mais cela a été rapide à mettre en œuvre et n’a nécessité que très peu de code personnalisé.