c++ multithreading condition-variable

c++ - ¿Tengo que adquirir un bloqueo antes de llamar a condition_variable.notify_one()?



multithreading condition-variable (6)

Situación

Usando vc10 y Boost 1.56 implementé una cola concurrente de manera muy similar a como lo sugiere esta publicación del blog . El autor desbloquea el mutex para minimizar la contención, es decir, se llama a notify_one() 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 }

Desbloquear el 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:

  • Si bien aún no se ha llamado a cond_.wait() todavía puede interrumpirse mediante boost::thread::interrupt()
  • una vez se llamó a notify_one() por primera vez cond_.wait() puntos muertos; la espera no se puede terminar con boost::thread::interrupt() o boost::condition_variable::notify_*() más.

Solución

Al eliminar la línea mlock.unlock() el código funcionó como se esperaba (las notificaciones y las interrupciones terminan la espera). Tenga en cuenta que se llama a notify_one() con el mutex aún bloqueado, se desbloquea justo después de salir del ámbito:

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 la implementación de mi subproceso en particular, el mutex no debe desbloquearse antes de llamar boost::condition_variable::notify_one() , aunque ambas formas parecen correctas.

Estoy un poco confundido sobre el uso de std::condition_variable . Entiendo que tengo que crear un unique_lock en un mutex antes de llamar condition_variable.wait() . Lo que no puedo encontrar es si también debo adquirir un bloqueo único antes de llamar a notify_one() o notify_all() .

Los ejemplos en cppreference.com son conflictivos. Por ejemplo, la página notify_one da este ejemplo:

#include <iostream> #include <condition_variable> #include <thread> #include <chrono> std::condition_variable cv; std::mutex cv_m; int i = 0; bool done = false; void waits() { std::unique_lock<std::mutex> lk(cv_m); std::cout << "Waiting... /n"; cv.wait(lk, []{return i == 1;}); std::cout << "...finished waiting. i == 1/n"; done = true; } void signals() { std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Notifying.../n"; cv.notify_one(); std::unique_lock<std::mutex> lk(cv_m); i = 1; while (!done) { lk.unlock(); std::this_thread::sleep_for(std::chrono::seconds(1)); lk.lock(); std::cerr << "Notifying again.../n"; cv.notify_one(); } } int main() { std::thread t1(waits), t2(signals); t1.join(); t2.join(); }

Aquí el bloqueo no se adquiere para el primer notify_one() , pero se adquiere para el segundo notify_one() . Mirando a través de otras páginas con ejemplos veo cosas diferentes, en su mayoría no adquiriendo el bloqueo.

  • ¿Puedo elegirme para bloquear el mutex antes de llamar a notify_one() , y por qué elegiría bloquearlo?
  • En el ejemplo dado, ¿por qué no hay bloqueo para el primer notify_one() , pero hay para las llamadas posteriores? ¿Es este ejemplo incorrecto o hay alguna razón?

@Michael Burr tiene razón. condition_variable::notify_one no requiere un bloqueo en la variable. Sin embargo, nada le impide usar un bloqueo 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 el subproceso de signals modifica la variable, debe asegurarse de que ningún otro subproceso tenga acceso a ella durante ese tiempo.

Los bloqueos se utilizan para cualquier situación que requiera sincronización , no creo que podamos afirmarlo de una manera más general.


Como han señalado otros, no es necesario mantener el bloqueo al llamar a notify_one() , en términos de condiciones de carrera y problemas relacionados con la notify_one() . Sin embargo, en algunos casos, puede ser necesario mantener el bloqueo para evitar que la condition_variable se destruya antes de que se notify_one() . 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 creado recientemente 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 hilo t adquiere el bloqueo (1), establece la variable predicada (2) y luego libera el bloqueo (3). Supongamos que hay otro cambio de contexto justo en este punto antes de que notify_one() (4) se ejecute. El hilo principal adquiere el bloqueo (6) y ejecuta la línea (7), momento en el que el predicado devuelve true y no hay razón para esperar, por lo que libera el bloqueo y continúa. foo devoluciones de foo (8) y las variables en su alcance (incluido cv ) se destruyen. Antes de que el hilo t pueda unirse al hilo principal (9), tiene que finalizar su ejecución, por lo que continúa desde donde lo dejó para ejecutar cv.notify_one() (4), ¡en cuyo punto el cv ya está destruido!

La solución posible en este caso es mantener el bloqueo al llamar a notify_one (es decir, eliminar el alcance que termina en la línea (3)). Al hacerlo, nos aseguramos de que el subproceso t llame a notify_one antes de que cv.wait pueda verificar la nueva variable de predicado establecida y continuar, ya que tendría que adquirir el bloqueo, que t está manteniendo actualmente, para realizar la comprobación. Por lo tanto, nos aseguramos de que el subproceso t no tenga acceso a cv después de que foo regrese.

Para resumir, el problema en este caso específico no es realmente sobre el subprocesamiento, sino sobre el tiempo de vida de las variables capturadas por referencia. cv se captura por referencia mediante el hilo t , por lo tanto, debe asegurarse de que cv permanezca vivo durante la ejecución del hilo. Los otros ejemplos presentados aquí no sufren este problema, ya condition_variable objetos de condition_variable mutex y de mutex se definen en el ámbito global, por lo que se garantiza que se mantendrán vivos hasta que el programa salga.


En algunos casos, cuando el cv puede estar ocupado (bloqueado) por otros hilos. Debe obtener el bloqueo y liberarlo antes de notificar a _ * ().
Si no, la notificación _ * () tal vez no se ejecute en absoluto.


No es necesario mantener un bloqueo al llamar a condition_variable::notify_one() , pero no es incorrecto en el sentido de que todavía es un comportamiento bien definido y no un error.

Sin embargo, podría ser un "pesimismo", ya que cualquier subproceso en espera que se pueda ejecutar (si lo hubiera) intentará de inmediato obtener el bloqueo que mantiene el subproceso de notificación. Creo que es una buena regla empírica para evitar mantener el bloqueo asociado con una variable de condición al llamar a notify_one() o notify_all() . Consulte Pthread Mutex: pthread_mutex_unlock () consume mucho tiempo para un ejemplo en el que al liberar un bloqueo antes de llamar al pthread equivalente de notify_one() mejoró el rendimiento.

Tenga en cuenta que la llamada de lock() en el bucle while es necesaria en algún momento, porque el bloqueo debe mantenerse durante la verificación de la condición del bucle while (!done) . Pero no es necesario notify_one() la llamada a 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 a la llamada a notify_one() . Sé que esta actualización es tardía 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 a notify_one() justo antes del consumidor ( waits() en este ejemplo) es capaz de llamar a wait() .

La clave es lo que le sucede a i : ese es el objeto que realmente indica si el consumidor tiene o no "trabajo" que hacer. La condition_variable es solo un mecanismo para permitir que el consumidor espere de manera eficiente un cambio en i .

El productor debe mantener el bloqueo al actualizar i , y el consumidor debe mantener el bloqueo al verificar i y llamar a condition_variable::wait() (si es necesario esperar). En este caso, la clave es que debe ser la misma instancia de mantener el bloqueo (a menudo llamado una sección crítica) cuando el consumidor hace esta comprobación y espera. Dado que la sección crítica se mantiene cuando el productor actualiza i y cuando el consumidor verifica y espera en i , no hay oportunidad para que cambie cuando el consumidor verifica i y cuando llama condition_variable::wait() . Este es el quid para 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, el consumidor llama a cv.wait() , entonces cv.wait() siendo 0 cuando se llame a la parte de wait(lock) de la implementación; el uso correcto de los bloqueos garantiza eso. En este caso, el productor no tiene la oportunidad de llamar a condition_variable::notify_one() en su bucle while hasta que el consumidor haya llamado a cv.wait(lk, []{return i == 1;}) (y la wait() la llamada ha hecho todo lo que necesita para "capturar" una notificación correctamente - wait() no liberará el bloqueo hasta que haya hecho eso. Así que en este caso, el consumidor no puede perderse la notificación.

  • si i ya es 1 cuando el consumidor llama a cv.wait() , la parte de wait(lock) de la implementación nunca se llamará porque la prueba while (!pred()) hará que el bucle interno finalice. En esta situación, no importa cuando se produce la llamada a notification_one (), el consumidor no bloqueará.

El ejemplo aquí tiene la complejidad adicional de usar la variable done para indicar al hilo productor que el consumidor ha reconocido que i == 1 , pero no creo que esto cambie el análisis en absoluto porque todo el acceso a done (tanto para la lectura como para la modificación) se realizan en las mismas secciones críticas que involucran a i y la condition_variable .

Si observas la pregunta a la que apunta @ eh9, Sync no es confiable al usar std :: atomic y std :: condition_variable , verás 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 comprobación y espera.

En ese ejemplo, el código se ve así:

if (--f->counter == 0) // (1) // we have zeroed this fence''s counter, wake up everyone that waits f->resume.notify_all(); // (2) else { unique_lock<mutex> lock(f->resume_mutex); f->resume.wait(lock); // (3) }

Notará que la wait() en el # 3 se realiza mientras mantiene presionado f->resume_mutex . Pero la verificación de si la wait() es necesaria o no en el paso 1 no se realiza mientras se mantiene ese bloqueo (mucho menos de manera continua para la verificación y espera), 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 de tipo std::atomic esto cumpliría el requisito. Sin embargo, la atomicidad proporcionada por std::atomic no se extiende a la llamada subsiguiente a f->resume.wait(lock) . En este ejemplo, hay una carrera entre cuando se comprueba f->counter (paso # 1) y cuando se llama a la wait() (paso # 3).

Esa raza no existe en el ejemplo de esta pregunta.


Simplemente agregue 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, DEBES tomar el bloqueo antes de entrar en espera (lk) porque wait () desbloquea lk y sería un comportamiento indefinido si el bloqueo no estuviera bloqueado. Este no es el caso con notify_one (), pero debe asegurarse de no llamar a Notify _ * () antes de ingresar a wait () y hacer que esa llamada desbloquee el mutex; lo que, obviamente, solo se puede hacer bloqueando el mismo mutex antes de que llames 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 subprocesos llaman a start () y stop () en pares, pero solo mientras start () devuelva true. Por ejemplo:

if (start()) { // Do stuff stop(); }

Una (otra) cadena 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, cancel () se supone que no debe regresar mientras haya subprocesos entre start () y stop (), y una vez que cancel () ejecute su primera línea, start () siempre devolverá false, por lo que no habrá nuevos threads que ingresen en el ''Do área de cosas

Funciona bien?

El razonamiento es como sigue:

1) Si algún hilo ejecuta con éxito la primera línea de inicio () (y, por lo tanto, devolverá verdadero), ningún hilo ejecutó la primera línea de cancelar () todavía (suponemos que el número total de hilos es mucho menor que 1000 por camino).

2) Además, mientras un hilo ejecutó con éxito la primera línea de inicio (), pero aún no la primera línea de detener (), es imposible que un hilo ejecute con éxito la primera línea de cancelar () (tenga en cuenta que solo un hilo ever call cancel ()): el valor devuelto por fetch_sub (1000) será mayor que 0.

3) Una vez que un subproceso ejecutó la primera línea de cancel (), la primera línea de inicio () siempre devolverá el valor falso y un subproceso que llama inicio () ya no entrará en el área "Hacer cosas".

4) El número de llamadas a start () y stop () siempre están equilibradas, 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 (la última) llamada a stop () haga que el conteo para llegar a -1000 y, por lo tanto, Notify_one () para ser llamado. Tenga en cuenta que solo puede suceder cuando la primera línea de cancelación haya provocado que ese hilo se caiga.

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 devuelve, lo que uno puede aceptar como "poco probable y nunca duradero", 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 que tome el mutex y / o 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 notification_one () se realiza ANTES de que estemos esperando () - en la variable de condición! Y el programa se bloquearía indefinidamente.

Por esta razón, no deberíamos poder llamar a Notify_one () hasta que llamemos a wait (). Tenga en cuenta que la potencia de una variable de condición reside en que es capaz de desbloquear el mutex de forma atómica, verifique si se produjo una llamada a notification_one () y vaya a dormir o no. No puede engañarlo, pero debe mantener el mutex bloqueado cada vez que realice cambios en las variables que podrían cambiar la condición de falso a verdadero y mantenerlo bloqueado al llamar a Notify_one () debido a las condiciones de carrera que se describen aquí.

En este ejemplo no hay condición sin embargo. ¿Por qué no usé como condición ''count == -1000''? Porque eso no es interesante en absoluto aquí: tan pronto como se alcanza -1000, estamos seguros de que ningún nuevo hilo entrará en el área ''Hacer cosas''. Además, los subprocesos todavía 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, por lo que estamos seguros de que ya no hay subprocesos en el área ''Hacer cosas''. Estamos seguros de que este es el caso cuando se llama a Notify_one (), pero ¿cómo asegurarnos de que no llamemos a Notify_one () antes de que cancel () bloquee su mutex? El simple hecho de bloquear cancel_mutex poco antes de Notify_one () no ayudará, por supuesto.

El problema es que, a pesar de que no estamos esperando una condición, todavía hay una condición y necesitamos bloquear el mutex.

1) antes de llegar a esa condición 2) antes de llamar a notification_one.

El código correcto por lo tanto se convierte en:

void stop() { if (count.fetch_sub(1) == -999) // Reached -1000 ? { cancel_mutex.lock(); cancel_mutex.unlock(); cv.notify_one(); } }

[... mismo inicio () ...]

void cancel() { std::unique_lock<std::mutex> lk(cancel_mutex); if (count.fetch_sub(1000) == 0) return; cancel_cv.wait(lk); }

Por supuesto, esto es solo un ejemplo, pero otros casos son muy parecidos; en casi todos los casos en los que utiliza una variable condicional, deberá tener ese mutex bloqueado (poco) 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 posibilidad (pequeña) de que la llamada a notify_one () despierte el hilo esperando la variable de condición que luego intentará tomar el mutex y Bloque, antes de soltar el mutex de nuevo. Eso es solo un poco más lento de lo necesario.

Este ejemplo fue algo especial porque la línea que cambia la condición se ejecuta en el mismo hilo que llama a wait ().

Más común es el caso en el que un hilo simplemente espera que una condición se convierta en realidad y otro hilo retenga el bloqueo antes de cambiar las variables involucradas en esa condición (lo que posiblemente se convierta en verdadero). En ese caso, la exclusión mutua se bloquea inmediatamente antes (y después) de que la condición se haya cumplido, por lo que es totalmente correcto desbloquear la exclusión mutua antes de llamar a notificar a _ * () en ese caso.