D'abord un peu contexte : Je suis en train d'apprendre à utiliser les threads en C++ 11 et, dans ce but, j'essaie de construire une petite classe actor
, essentiellement (j'ai laissé le matériel de gestion et de propagation d'exceptions comme):
class actor {
private: std::atomic<bool> stop;
private: std::condition_variable interrupt;
private: std::thread actor_thread;
private: message_queue incoming_msgs;
public: actor()
: stop(false),
actor_thread([&]{ run_actor(); })
{}
public: virtual ~actor() {
// if the actor is destroyed, we must ensure the thread dies too
stop = true;
// to this end, we have to interrupt the actor thread which is most probably
// waiting on the incoming_msgs queue:
interrupt.notify_all();
actor_thread.join();
}
private: virtual void run_actor() {
try {
while(!stop)
// wait for new message and process it
// but interrupt the waiting process if interrupt is signaled:
process(incoming_msgs.wait_and_pop(interrupt));
}
catch(interrupted_exception) {
// ...
}
};
private: virtual void process(const message&) = 0;
// ...
};
Chaque acteur s'exécute dans son propre actor_thread
, attend un nouveau message entrant le incoming_msgs
et le traite lorsqu'un message arrive.
Le actor_thread
est créé avec la actor
et doit mourir en même temps. C'est pourquoi j'ai besoin d'un mécanisme d'interruption dans la message_queue::wait_and_pop(std::condition_variable interrupt)
.
J'exige essentiellement que wait_and_pop
bloque jusqu'à ce que A) une nouvelle message
arrive ou B) jusqu'à ce que la interrupt
soit déclenchée, auquel cas, idéalement, un interrupted_exception
doit être lancé.
L’arrivée d’un nouveau message dans le message_queue
est actuellement aussi modélisée par un std::condition_variable new_msg_notification
:
// ...
// in class message_queue:
message wait_and_pop(std::condition_variable& interrupt) {
std::unique_lock<std::mutex> lock(mutex);
// How to interrupt the following, when interrupt fires??
new_msg_notification.wait(lock,[&]{
return !queue.empty();
});
auto msg(std::move(queue.front()));
queue.pop();
return msg;
}
Pour résumer la longue histoire, la question est la suivante: comment puis-je interrompre l’attente d’un nouveau message dans new_msg_notification.wait(...)
lorsque la interrupt
est déclenchée (sans introduire de délai d’expiration)?
Sinon, la question peut être lue comme suit: Comment puis-je attendre que l’un des deux std::condition_variable
s soit signalé?
Une approche naïve semble être de ne pas utiliser std::condition_variable
du tout pour l’interruption mais d’utiliser plutôt un indicateur atomique std::atomic<bool> interrupted
puis occupé, attendez new_msg_notification
avec un très petit délai d’attente jusqu’à ce qu’un nouveau message soit arrivé ou jusqu’au true==interrupted
. Cependant, j'aimerais beaucoup éviter d'attendre les travaux.
D'après les commentaires et la réponse de Pilcrow, il semble qu'il y a fondamentalement deux approches possibles.
Pour ceux qui sont intéressés, ma mise en œuvre va comme suit. La variable de condition dans mon cas est en fait une semaphore
(parce que je les aime plus et parce que j'aime l'exercice). J'ai équipé ce sémaphore avec une variable interrupt
associée pouvant être obtenue à partir du sémaphore via semaphore::get_interrupt()
. Si maintenant un thread est bloqué dans semaphore::wait()
, un autre thread a la possibilité d'appeler semaphore::interrupt::trigger()
lors de l'interruption du sémaphore, provoquant le déblocage du premier thread et la propagation d'un interrupt_exception
.
struct
interrupt_exception {};
class
semaphore {
public: class interrupt;
private: mutable std::mutex mutex;
// must be declared after our mutex due to construction order!
private: interrupt* informed_by;
private: std::atomic<long> counter;
private: std::condition_variable cond;
public:
semaphore();
public:
~semaphore() throw();
public: void
wait();
public: interrupt&
get_interrupt() const { return *informed_by; }
public: void
post() {
std::lock_guard<std::mutex> lock(mutex);
counter++;
cond.notify_one(); // never throws
}
public: unsigned long
load () const {
return counter.load();
}
};
class
semaphore::interrupt {
private: semaphore *forward_posts_to;
private: std::atomic<bool> triggered;
public:
interrupt(semaphore *forward_posts_to) : triggered(false), forward_posts_to(forward_posts_to) {
assert(forward_posts_to);
std::lock_guard<std::mutex> lock(forward_posts_to->mutex);
forward_posts_to->informed_by = this;
}
public: void
trigger() {
assert(forward_posts_to);
std::lock_guard<std::mutex>(forward_posts_to->mutex);
triggered = true;
forward_posts_to->cond.notify_one(); // never throws
}
public: bool
is_triggered () const throw() {
return triggered.load();
}
public: void
reset () throw() {
return triggered.store(false);
}
};
semaphore::semaphore() : counter(0L), informed_by(new interrupt(this)) {}
// must be declared here because otherwise semaphore::interrupt is an incomplete type
semaphore::~semaphore() throw() {
delete informed_by;
}
void
semaphore::wait() {
std::unique_lock<std::mutex> lock(mutex);
if(0L==counter) {
cond.wait(lock,[&]{
if(informed_by->is_triggered())
throw interrupt_exception();
return counter>0;
});
}
counter--;
}
En utilisant cette semaphore
, mon implémentation de file d'attente de messages ressemble maintenant à ceci (en utilisant le sémaphore au lieu du std::condition_variable
, je pourrais supprimer le std::mutex
:
class
message_queue {
private: std::queue<message> queue;
private: semaphore new_msg_notification;
public: void
Push(message&& msg) {
queue.Push(std::move(msg));
new_msg_notification.post();
}
public: const message
wait_and_pop() {
new_msg_notification.wait();
auto msg(std::move(queue.front()));
queue.pop();
return msg;
}
public: semaphore::interrupt&
get_interrupt() const { return new_msg_notification.get_interrupt(); }
};
Ma actor
est maintenant capable d'interrompre son thread avec une latence très faible dans son thread. La mise en œuvre actuellement ressemble à ceci:
class
actor {
private: message_queue
incoming_msgs;
/// must be declared after incoming_msgs due to construction order!
private: semaphore::interrupt&
interrupt;
private: std::thread
my_thread;
private: std::exception_ptr
exception;
public:
actor()
: interrupt(incoming_msgs.get_interrupt()), my_thread(
[&]{
try {
run_actor();
}
catch(...) {
exception = std::current_exception();
}
})
{}
private: virtual void
run_actor() {
while(!interrupt.is_triggered())
process(incoming_msgs.wait_and_pop());
};
private: virtual void
process(const message&) = 0;
public: void
notify(message&& msg_in) {
incoming_msgs.Push(std::forward<message>(msg_in));
}
public: virtual
~actor() throw (interrupt_exception) {
interrupt.trigger();
my_thread.join();
if(exception)
std::rethrow_exception(exception);
}
};
Tu demandes,
Quel est le meilleur moyen d'attendre plusieurs variables de condition en C++ 11?
Vous ne pouvez pas et devez refondre. Un thread ne peut attendre que sur une variable de condition (et son mutex associé) à la fois. À cet égard, les installations Windows pour la synchronisation sont plutôt plus riches que celles de la famille de primitives de synchronisation "de style POSIX".
L'approche typique avec les files d'attente thread-safe est de mettre en file d'attente une spéciale "tout est fait!" message, ou pour concevoir une file d'attente "cassable" (ou "shutdown -able"). Dans ce dernier cas, la variable de condition interne de la file protège alors un prédicat complexe: un élément est disponible ou la file a été interrompue.
Dans un commentaire, vous remarquez que
un notify_all () n'aura aucun effet s'il n'y a personne en attente
C'est vrai mais probablement pas pertinent. wait()
ing sur une variable de condition implique également la vérification d'un prédicat et son contrôle avant bloquant réellement une notification. Ainsi, un thread de travail occupé à traiter un élément de la file d'attente qui "manque" une notify_all()
verra, lors de la prochaine inspection de la condition de la file d'attente, que le prédicat (un nouvel élément est disponible ou que la file d'attente est terminée) a été modifié.
Récemment, j'ai résolu ce problème à l'aide d'une variable de condition unique et d'une variable booléenne distincte pour chaque producteur/travailleur. Le prédicat de la fonction wait dans le thread consommateur peut vérifier ces indicateurs et décider quel producteur/travailleur a satisfait à la condition.
Peut-être que cela peut fonctionner:
se débarrasser de l'interruption.
message wait_and_pop(std::condition_variable& interrupt) {
std::unique_lock<std::mutex> lock(mutex);
{
new_msg_notification.wait(lock,[&]{
return !queue.empty() || stop;
});
if( !stop )
{
auto msg(std::move(queue.front()));
queue.pop();
return msg;
}
else
{
return NULL; //or some 'terminate' message
}
}
Dans destructeur, remplacez interrupt.notify_all()
par new_msg_notification.notify_all()