No es necesario mantener un candado al llamar condition_variable::notify_one()
, pero no está mal en el sentido de que sigue siendo un comportamiento bien definido y no un error.
Sin embargo, podría ser una "pesimización", ya que cualquier hilo de espera que se haga ejecutable (si lo hay) intentará adquirir inmediatamente el bloqueo que mantiene el hilo de notificación. Creo que es una buena regla para evitar mantener el bloqueo asociado con una variable de condición mientras se llama a notify_one()
o notify_all()
. Consulte Pthread Mutex: pthread_mutex_unlock () consume mucho tiempo para ver un ejemplo en el que liberar un bloqueo antes de llamar al equivalente pthread de notify_one()
rendimiento mejorado de forma medible.
Tenga en cuenta que la lock()
llamada en el while
bucle es necesaria en algún momento, porque el bloqueo debe mantenerse durante la while (!done)
verificación de la condición del bucle. Pero no es necesario retenerlo para que la llamada notify_one()
.
2016-02-27 : Actualización grande para abordar algunas preguntas en los comentarios sobre si hay una condición de carrera si el bloqueo no ayuda para la notify_one()
llamada. Sé que esta actualización llega tarde porque la pregunta se hizo hace casi dos años, pero me gustaría abordar la pregunta de @ Cookie sobre una posible condición de carrera si el productor ( signals()
en este ejemplo) llama notify_one()
justo antes que el consumidor ( waits()
en este ejemplo). capaz de llamar wait()
.
La clave es lo que le sucede i
, ese es el objeto que realmente indica si el consumidor tiene "trabajo" que hacer o no. El condition_variable
es simplemente un mecanismo para que el consumidor espera de manera eficiente para que un cambio i
.
El productor debe mantener el bloqueo al actualizar i
, y el consumidor debe mantener el bloqueo mientras verifica i
y llama condition_variable::wait()
(si es que necesita esperar). En este caso, la clave es que debe ser la misma instancia de mantener la cerradura (a menudo llamada sección crítica) cuando el consumidor realiza esta verificación y espera. Dado que la sección crítica se lleva a cabo cuando el productor actualiza i
y cuando el consumidor verifica y espera i
, no hay oportunidad de i
cambiar entre el momento en que el consumidor verifica i
y cuando llama condition_variable::wait()
. Este es el quid de un uso adecuado de las variables de condición.
El estándar de C ++ dice que condition_variable :: wait () se comporta de la siguiente manera cuando se llama con un predicado (como en este caso):
while (!pred())
wait(lock);
Hay dos situaciones que pueden ocurrir cuando el consumidor verifica i
:
si i
es 0, entonces el consumidor llama cv.wait()
, entonces i
seguirá siendo 0 cuando wait(lock)
se llame a la parte de la implementación; el uso adecuado de los bloqueos lo garantiza. En este caso, el productor no tiene la oportunidad de llamar al condition_variable::notify_one()
en su while
bucle hasta después de que el consumidor haya llamado cv.wait(lk, []{return i == 1;})
(y la wait()
llamada haya hecho todo lo necesario para 'capturar' correctamente una notificación; wait()
no liberará el bloqueo hasta que lo haya hecho ). Entonces, en este caso, el consumidor no puede perderse la notificación.
si i
ya es 1 cuando el consumidor llama cv.wait()
, la wait(lock)
parte de la implementación nunca se llamará porque la while (!pred())
prueba hará que el ciclo interno termine. En esta situación, no importa cuándo ocurre la llamada a notify_one (), el consumidor no bloqueará.
El ejemplo aquí tiene la complejidad adicional de usar la done
variable para indicarle al hilo del productor que el consumidor ha reconocido eso i == 1
, pero no creo que esto cambie el análisis en absoluto porque todo el acceso a done
(tanto para leer como para modificar ) se realizan mientras se encuentran en las mismas secciones críticas que involucran i
y el condition_variable
.
Si nos fijamos en la cuestión de que @ EH9 señaló, la sincronización no es fiable utilizando std :: atómica y std :: condition_variable , que van a ver una condición de carrera. Sin embargo, el código publicado en esa pregunta viola una de las reglas fundamentales del uso de una variable de condición: no contiene una sola sección crítica cuando se realiza una verificación y espera.
En ese ejemplo, el código se ve así:
if (--f->counter == 0)
f->resume.notify_all();
else
{
unique_lock<mutex> lock(f->resume_mutex);
f->resume.wait(lock);
}
Notará que wait()
en # 3 se realiza mientras se sostiene f->resume_mutex
. Pero la verificación de si wait()
es necesario o no en el paso n. ° 1 no se realiza mientras se mantiene ese bloqueo (mucho menos de forma continua para verificar y esperar), que es un requisito para el uso adecuado de las variables de condición). Creo que la persona que tiene el problema con ese fragmento de código pensó que, dado que f->counter
era un std::atomic
tipo, cumpliría con el requisito. Sin embargo, la atomicidad proporcionada por std::atomic
no se extiende a la siguiente llamada a f->resume.wait(lock)
. En este ejemplo, hay una carrera entre cuándo f->counter
se marca (paso n. ° 1) y cuándo wait()
se llama (paso n. ° 3).
Esa raza no existe en el ejemplo de esta pregunta.
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.the_condition_variable.wait(lock);
. Si no se necesita un bloqueo para sincronizar el productor y el consumidor (digamos que el subyacente es una cola spsc sin bloqueo), entonces ese bloqueo no sirve para nada si el productor no lo bloquea. Bien por mi. ¿Pero no existe el riesgo de una raza rara? Si el productor no mantiene el candado, ¿no podría llamar a notify_one mientras el consumidor está justo antes de la espera? Entonces, el consumidor espera y no se despierta ...std::cout << "Waiting... \n";
mientras que el productor lo hacecv.notify_one();
, luego la llamada de atención desaparece ... ¿O me estoy perdiendo algo aquí?Situación
Usando vc10 y Boost 1.56, implementé una cola concurrente muy parecida a como sugiere esta publicación de blog . El autor desbloquea el mutex para minimizar la contención, es decir,
notify_one()
se llama con el mutex desbloqueado:void push(const T& item) { std::unique_lock<std::mutex> mlock(mutex_); queue_.push(item); mlock.unlock(); // unlock before notificiation to minimize mutex contention cond_.notify_one(); // notify one waiting thread }
El desbloqueo del mutex está respaldado por un ejemplo en la documentación de Boost :
void prepare_data_for_processing() { retrieve_data(); prepare_data(); { boost::lock_guard<boost::mutex> lock(mut); data_ready=true; } cond.notify_one(); }
Problema
Aún así, esto llevó al siguiente comportamiento errático:
notify_one()
aún no se ha llamado todavíacond_.wait()
se puede interrumpir a través deboost::thread::interrupt()
notify_one()
se llamó por primera vezcond_.wait()
puntos muertos; la espera no puede terminarboost::thread::interrupt()
niboost::condition_variable::notify_*()
más.Solución
Eliminar la línea
mlock.unlock()
hizo que el código funcionara como se esperaba (las notificaciones y las interrupciones terminan la espera). Tenga en cuenta quenotify_one()
se llama con el mutex aún bloqueado, se desbloquea inmediatamente después de salir del alcance:void push(const T& item) { std::lock_guard<std::mutex> mlock(mutex_); queue_.push(item); cond_.notify_one(); // notify one waiting thread }
Eso significa que al menos con mi implementación de hilo particular, el mutex no debe desbloquearse antes de llamar
boost::condition_variable::notify_one()
, aunque ambas formas parecen correctas.fuente
Como han señalado otros, no es necesario mantener el candado al llamar
notify_one()
, en términos de condiciones de carrera y problemas relacionados con el subproceso. Sin embargo, en algunos casos, es posible que sea necesario sujetar el candado para evitarcondition_variable
que se destruya antes denotify_one()
llamar. Considere el siguiente ejemplo:thread t; void foo() { std::mutex m; std::condition_variable cv; bool done = false; t = std::thread([&]() { { std::lock_guard<std::mutex> l(m); // (1) done = true; // (2) } // (3) cv.notify_one(); // (4) }); // (5) std::unique_lock<std::mutex> lock(m); // (6) cv.wait(lock, [&done]() { return done; }); // (7) } void main() { foo(); // (8) t.join(); // (9) }
Supongamos que hay un cambio de contexto al hilo recién creado
t
después de que lo creamos, pero antes de que comencemos a esperar en la variable de condición (en algún lugar entre (5) y (6)). El hilot
adquiere el bloqueo (1), establece la variable de predicado (2) y luego libera el bloqueo (3). Suponga que hay otro cambio de contexto justo en este punto antesnotify_one()
de que se ejecute (4). El hilo principal adquiere el bloqueo (6) y ejecuta la línea (7), momento en el que el predicado regresatrue
y no hay razón para esperar, por lo que libera el bloqueo y continúa.foo
devuelve (8) y las variables en su alcance (incluidascv
) se destruyen. Antes de que el hilot
pueda unirse al hilo principal (9), tiene que terminar su ejecución, por lo que continúa desde donde lo dejó para ejecutarsecv.notify_one()
(4), momento en el quecv
ya está destruido!La posible solución en este caso es mantener el candado al llamar
notify_one
(es decir, eliminar el alcance que termina en la línea (3)). Al hacerlo, nos aseguramos de que last
llamadas a subprocesosnotify_one
anteriorescv.wait
puedan verificar la variable de predicado recién configurada y continuar, ya que necesitaría adquirir el bloqueo, quet
actualmente se mantiene, para hacer la verificación. Por lo tanto, nos aseguramos de quecv
no se acceda por hilot
después de lasfoo
devoluciones.En resumen, el problema en este caso específico no se trata realmente de subprocesos, sino de la vida útil de las variables capturadas por referencia.
cv
se captura por referencia a través de un hilot
, por lo tanto, debe asegurarse de que secv
mantenga activo durante la ejecución del hilo. Los otros ejemplos presentados aquí no sufren este problema, porque los objetoscondition_variable
ymutex
se definen en el alcance global, por lo que se garantiza que se mantendrán vivos hasta que el programa salga.fuente
@Michael Burr tiene razón.
condition_variable::notify_one
no requiere un bloqueo en la variable. Sin embargo, nada le impide usar un candado en esa situación, como lo ilustra el ejemplo.En el ejemplo dado, el bloqueo está motivado por el uso concurrente de la variable
i
. Debido a que elsignals
hilo modifica la variable, debe asegurarse de que ningún otro hilo tenga acceso a él durante ese tiempo.Los bloqueos se utilizan para cualquier situación que requiera sincronización , no creo que podamos expresarlo de una manera más general.
fuente
wait
función de variable de condición está liberando el bloqueo dentro de la llamada, y regresa solo después de haber vuelto a adquirir el bloqueo. después de lo cual puede verificar su condición de manera segura porque ha adquirido los "derechos de lectura", digamos. si aún no es lo que estás esperando, vuelve await
. este es el patrón. por cierto, este ejemplo NO lo respeta.En algún caso, cuando el CV puede estar ocupado (bloqueado) por otros hilos. Debe bloquearlo y liberarlo antes de notificar a _ * ().
De lo contrario, la notificación _ * () tal vez no se ejecute en absoluto.
fuente
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) // Reached -1000 ? cv.notify_one(); } bool start() { if (count.fetch_add(1) >= 0) return true; // Failure. stop(); return false; } void cancel() { if (count.fetch_sub(1000) == 0) // Reached -1000? return; // Wait till count reached -1000. 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()) { // Do stuff 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) // Reached -1000 ? { 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.
fuente