Solo agrego esta respuesta porque creo que la respuesta aceptada podría ser engañosa. En todos los casos, deberá bloquear el mutex, antes de llamar a notify_one () en algún lugar para que su código sea seguro para subprocesos, aunque puede desbloquearlo nuevamente antes de llamar a notify_ * ().
Para aclarar, DEBE tomar el bloqueo antes de ingresar a wait (lk) porque wait () desbloquea lk y sería un comportamiento indefinido si el bloqueo no estuviera bloqueado. Este no es el caso de notify_one (), pero debe asegurarse de no llamar a notify _ * () antes de ingresar wait () y hacer que esa llamada desbloquee el mutex; lo que obviamente solo se puede hacer bloqueando ese mismo mutex antes de llamar a notify _ * ().
Por ejemplo, considere el siguiente caso:
std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;
void stop()
{
if (count.fetch_sub(1) == -999)
cv.notify_one();
}
bool start()
{
if (count.fetch_add(1) >= 0)
return true;
stop();
return false;
}
void cancel()
{
if (count.fetch_sub(1000) == 0)
return;
std::unique_lock<std::mutex> lk(cancel_mutex);
cancel_cv.wait(lk);
}
Advertencia : este código contiene un error.
La idea es la siguiente: los hilos llaman a start () y stop () en pares, pero solo mientras start () devuelva verdadero. Por ejemplo:
if (start())
{
stop();
}
Un (otro) hilo en algún momento llamará a cancel () y después de regresar de cancel () destruirá los objetos que se necesitan en 'Hacer cosas'. Sin embargo, se supone que cancel () no regresará mientras haya subprocesos entre start () y stop (), y una vez que cancel () ejecutó su primera línea, start () siempre devolverá falso, por lo que ningún subproceso nuevo ingresará al 'Do área de cosas.
¿Funciona bien?
El razonamiento es como sigue:
1) Si algún hilo ejecuta con éxito la primera línea de start () (y por lo tanto devolverá verdadero) entonces ningún hilo ejecutó la primera línea de cancel () todavía (asumimos que el número total de hilos es mucho menor que 1000 por el camino).
2) Además, mientras un hilo ejecutó con éxito la primera línea de start (), pero aún no la primera línea de stop (), entonces es imposible que cualquier hilo ejecute con éxito la primera línea de cancel () (tenga en cuenta que solo un hilo siempre llama a cancel ()): el valor devuelto por fetch_sub (1000) será mayor que 0.
3) Una vez que un hilo ejecutó la primera línea de cancel (), la primera línea de start () siempre devolverá falso y un hilo que llame a start () ya no entrará en el área 'Hacer cosas'.
4) El número de llamadas a start () y stop () siempre está equilibrado, por lo que después de que la primera línea de cancel () se ejecute sin éxito, siempre habrá un momento en el que una (última) llamada a stop () provoque la cuenta para llegar a -1000 y, por lo tanto, notificar a uno () para ser llamado. Tenga en cuenta que eso solo puede suceder cuando la primera línea de cancelación dio como resultado que ese hilo se cayera.
Aparte de un problema de inanición en el que tantos subprocesos llaman a start () / stop () que el recuento nunca llega a -1000 y cancel () nunca regresa, lo que uno podría aceptar como "improbable y que nunca durará mucho", hay otro error:
Es posible que haya un hilo dentro del área 'Hacer cosas', digamos que solo está llamando a stop (); en ese momento, un hilo ejecuta la primera línea de cancel () leyendo el valor 1 con fetch_sub (1000) y cayendo. ¡Pero antes de tomar el mutex y / o hacer la llamada a esperar (lk), el primer hilo ejecuta la primera línea de stop (), lee -999 y llama a cv.notify_one ()!
¡Entonces esta llamada a notify_one () se hace ANTES de que estemos esperando () - en la variable de condición! Y el programa se bloqueará indefinidamente.
Por esta razón no deberíamos poder llamar a notify_one () hasta que llamemos a wait (). Tenga en cuenta que el poder de una variable de condición radica en que puede desbloquear atómicamente el mutex, verificar si ocurrió una llamada a notify_one () e irse a dormir o no. No se puede engañar, pero que hacerlo necesidad de mantener el mutex bloqueado cada vez que se realizan cambios en las variables que podrían cambiar la condición de falso a verdadero y mantener bajo llave mientras llama notify_one () a causa de las condiciones de carrera, como se describe aquí.
Sin embargo, en este ejemplo no existe ninguna condición. ¿Por qué no utilicé como condición 'count == -1000'? Porque eso no es nada interesante aquí: tan pronto como se alcance -1000, estamos seguros de que ningún hilo nuevo ingresará al área 'Hacer cosas'. Además, los subprocesos aún pueden llamar a start () e incrementarán el recuento (a -999 y -998, etc.) pero eso no nos importa. Lo único que importa es que se alcanzó -1000, para que sepamos con certeza que ya no hay hilos en el área 'Hacer cosas'. Estamos seguros de que este es el caso cuando se llama a notify_one (), pero ¿cómo asegurarnos de no llamar a notify_one () antes de que cancel () bloquee su mutex? Por supuesto, bloquear cancel_mutex poco antes de notify_one () no ayudará.
El problema es que, a pesar de que no estamos esperando una condición, todavía existe una condición y necesitamos bloquear el mutex
1) antes de que se alcance esa condición 2) antes de llamar a notify_one.
Por tanto, el código correcto se convierte en:
void stop()
{
if (count.fetch_sub(1) == -999)
{
cancel_mutex.lock();
cancel_mutex.unlock();
cv.notify_one();
}
}
[... mismo comienzo () ...]
void cancel()
{
std::unique_lock<std::mutex> lk(cancel_mutex);
if (count.fetch_sub(1000) == 0)
return;
cancel_cv.wait(lk);
}
Por supuesto, este es solo un ejemplo, pero otros casos son muy parecidos; en casi todos los casos en los que use una variable condicional, necesitará tener ese mutex bloqueado (en breve) antes de llamar a notify_one (), o de lo contrario es posible que lo llame antes de llamar a wait ().
Tenga en cuenta que desbloqueé el mutex antes de llamar a notify_one () en este caso, porque de lo contrario existe la (pequeña) posibilidad de que la llamada a notify_one () despierte el hilo esperando la variable de condición que luego intentará tomar el mutex y block, antes de que liberemos el mutex nuevamente. Eso es un poco más lento de lo necesario.
Este ejemplo fue un poco especial porque la línea que cambia la condición es ejecutada por el mismo hilo que llama a wait ().
Más habitual es el caso en el que un subproceso simplemente espera a que una condición se convierta en verdadera y otro subproceso toma el bloqueo antes de cambiar las variables involucradas en esa condición (haciendo que posiblemente se convierta en verdadera). En ese caso, el mutex se bloquea inmediatamente antes (y después) de que la condición se cumpla, por lo que está totalmente bien desbloquear el mutex antes de llamar a notify _ * () en ese caso.
wait morphing
optimización) Regla de oro explicada en este enlace: notificar CON bloqueo es mejor en situaciones con más de 2 hilos para obtener resultados más predecibles.