pthread_cond_wait - pthread_mutex_t
¿Por qué las funciones variables de condición de pthreads requieren un mutex? (9)
Es solo la forma en que las variables de condición se implementan (o se implementaron originalmente).
El mutex se usa para proteger la variable de condición en sí . Es por eso que lo necesitas bloqueado antes de esperar.
La espera destrabará "atómicamente" el mutex, permitiendo que otros accedan a la variable de condición (para señalización). Luego, cuando la variable de condición es señalizada o transmitida a, uno o más de los hilos en la lista de espera serán despertados y el mutex se bloqueará mágicamente nuevamente para ese hilo.
Por lo general, ve la siguiente operación con variables de condición, que ilustra cómo funcionan. El siguiente ejemplo es un hilo de trabajo al que se le da trabajo a través de una señal a una variable de condición.
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
do the work.
unlock mutex.
clean up.
exit thread.
El trabajo se realiza dentro de este ciclo, siempre que haya algunos disponibles cuando la espera regrese. Cuando el hilo ha sido marcado para dejar de hacer el trabajo (generalmente por otro hilo que establece la condición de salida y luego patea la variable de condición para activar este subproceso), el ciclo saldrá, el mutex se desbloqueará y este hilo saldrá.
El código anterior es un modelo de consumidor único ya que el mutex permanece bloqueado mientras se realiza el trabajo. Para una variación de varios consumidores, puede usar, por ejemplo :
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
copy work to thread local storage.
unlock mutex.
do the work.
lock mutex.
unlock mutex.
clean up.
exit thread.
lo que permite a otros consumidores recibir trabajo mientras éste está trabajando.
La variable de condición lo alivia de la carga de sondear alguna condición y, en cambio, permite que otro subproceso le avise cuando algo tiene que suceder. Otro hilo puede decir que el hilo que funciona está disponible de la siguiente manera:
lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
La gran mayoría de lo que a menudo se denomina erróneamente wakeup espías generalmente siempre se debía a que se habían señalado varios subprocesos dentro de su llamada pthread_cond_wait
(difusión), uno volvería con el mutex, haría el trabajo y luego volvería a esperar.
Entonces, el segundo hilo señalado podría salir cuando no había trabajo por hacer. Por lo tanto, debe tener una variable adicional que indique que se debe realizar el trabajo (esto fue intrínsecamente protegido contra mutex con el par condvar / mutex aquí; otros hilos necesarios para bloquear el mutex antes de cambiarlo).
Técnicamente era posible que un hilo volviera de una espera de condición sin ser expulsado por otro proceso (esto es un despertar espurio genuino) pero, en todos mis muchos años trabajando en pthreads, tanto en el desarrollo / servicio del código y como usuario de ellos, nunca recibí uno de estos. Quizás fue solo porque HP tuvo una implementación decente :-)
En cualquier caso, el mismo código que manejaba el caso erróneo también manejaba auténticos despertares espurios, ya que el indicador de disponibilidad de trabajo no se establecería para ellos.
Estoy leyendo en pthread.h
; las funciones relacionadas con la variable condición (como pthread_cond_wait(3)
) requieren un mutex como argumento. ¿Por qué? Por lo que puedo decir, ¿voy a crear un mutex solo para usar como argumento? ¿Qué se supone que debe hacer ese mutex?
Hice un ejercicio en clase si quieres un ejemplo real de variable de condición:
#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"
int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;
void attenteSeuil(arg)
{
pthread_mutex_lock(&mutex_compteur);
while(compteur < 10)
{
printf("Compteur : %d<10 so i am waiting.../n", compteur);
pthread_cond_wait(&varCond, &mutex_compteur);
}
printf("I waited nicely and now the compteur = %d/n", compteur);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
void incrementCompteur(arg)
{
while(1)
{
pthread_mutex_lock(&mutex_compteur);
if(compteur == 10)
{
printf("Compteur = 10/n");
pthread_cond_signal(&varCond);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
else
{
printf("Compteur ++/n");
compteur++;
}
pthread_mutex_unlock(&mutex_compteur);
}
}
int main(int argc, char const *argv[])
{
int i;
pthread_t threads[2];
pthread_mutex_init(&mutex_compteur, NULL);
pthread_create(&threads[0], NULL, incrementCompteur, NULL);
pthread_create(&threads[1], NULL, attenteSeuil, NULL);
pthread_exit(NULL);
}
Las variables de condición POSIX son sin estado. Por lo tanto, es su responsabilidad mantener el estado. Debido a que se accederá al estado mediante ambos subprocesos que esperan e hilos que indican a otros hilos que dejen de esperar, debe estar protegido por un mutex. Si crees que puedes usar variables de condición sin un mutex, entonces no has comprendido que las variables de condición son sin estado.
Las variables de condición se construyen alrededor de una condición. Los subprocesos que esperan una variable de condición esperan alguna condición. Los hilos que señalan las variables de condición cambian esa condición. Por ejemplo, un hilo puede estar esperando que lleguen algunos datos. Algún otro hilo podría notar que los datos han llegado. "La información ha llegado" es la condición.
Aquí está el uso clásico de una variable de condición, simplificada:
while(1)
{
pthread_mutex_lock(&work_mutex);
while (work_queue_empty()) // wait for work
pthread_cond_wait(&work_cv, &work_mutex);
work = get_work_from_queue(); // get work
pthread_mutex_unlock(&work_mutex);
do_work(work); // do that work
}
Vea cómo el hilo está esperando trabajo. El trabajo está protegido por un mutex. La espera libera el mutex para que otro hilo pueda darle un poco de trabajo a este hilo. Así es como se señalaría:
void AssignWork(WorkItem work)
{
pthread_mutex_lock(&work_mutex);
add_work_to_queue(work); // put work item on queue
pthread_cond_signal(&work_cv); // wake worker thread
pthread_mutex_unlock(&work_mutex);
}
Tenga en cuenta que necesita el mutex para proteger la cola de trabajo. Tenga en cuenta que la variable de condición en sí no tiene idea de si hay trabajo o no. Es decir, una variable de condición debe estar asociada a una condición, esa condición debe ser mantenida por su código, y dado que se comparte entre subprocesos, debe estar protegida por un mutex.
Las variables de condición están asociadas con un mutex porque es la única forma en que puede evitar la carrera que está diseñada para evitar.
// incorrect usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
pthread_mutex_unlock(&mutex);
if (ready) {
doWork();
} else {
pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
Now, lets look at a particularly nasty interleaving of these operations
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
if (ready) {
pthread_cond_wait(&cond1); // uh o!
En este punto, no hay ningún hilo que señale la variable de condición, por lo que thread1 esperará por siempre, aunque el protectedReadyToRunVariable dice que está listo para funcionar.
La única forma de evitar esto es que las variables de condición liberen el mutex de forma atómica al mismo tiempo que comienzan a esperar la variable de condición. Esta es la razón por la cual la función cond_wait requiere un mutex
// correct usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
if (ready) {
pthread_mutex_unlock(&mutex);
doWork();
} else {
pthread_cond_wait(&mutex, &cond1);
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);
No encuentro que las otras respuestas sean tan concisas y legibles como esta página . Normalmente, el código de espera se ve así:
mutex.lock()
while(!check())
condition.wait()
mutex.unlock()
Hay tres razones para ajustar la wait()
en un mutex:
- sin un mutex, otro hilo podría
signal()
antes de lawait()
y perderíamos este despertar. - normalmente
check()
depende de la modificación de otro hilo, por lo que necesita exclusión mutua de todos modos. - para garantizar que el hilo de mayor prioridad procede primero (la cola para el mutex permite al planificador decidir quién va después).
El tercer punto no siempre es una preocupación: el contexto histórico está vinculado desde el artículo a esta conversación .
A menudo se mencionan los despertadores espurios con respecto a este mecanismo (es decir, el hilo de espera se despierta sin signal()
se llame a la signal()
). Sin embargo, dichos eventos son manejados por el check()
bucle check()
.
No todas las funciones de variables de condición requieren un mutex: solo las operaciones de espera lo hacen. Las operaciones de señal y difusión no requieren un mutex. Una variable de condición tampoco está permanentemente asociada con un mutex específico; el mutex externo no protege la variable de condición. Si una variable de condición tiene un estado interno, como una cola de hilos en espera, debe estar protegida por un bloqueo interno dentro de la variable de condición.
Las operaciones de espera reúnen una variable de condición y un mutex, porque:
- un hilo ha bloqueado el mutex, ha evaluado algunas expresiones sobre variables compartidas y ha encontrado que es falso, por lo que debe esperar.
- el hilo debe pasar atómicamente de poseer el mutex a esperar en la condición.
Por esta razón, la operación de espera toma como argumentos tanto el mutex como la condición: para que pueda gestionar la transferencia atómica de un hilo desde la propiedad del mutex hasta la espera, para que el hilo no sea víctima de la condición de carrera de activación perdida .
Se producirá una condición de carrera de activación perdida si un subproceso abandona un mutex, y luego espera en un objeto de sincronización sin estado, pero de una manera que no es atómica: existe un intervalo de tiempo cuando el subproceso ya no tiene el bloqueo, y tiene aún no comenzado a esperar en el objeto. Durante esta ventana, puede entrar otro hilo, hacer que la condición esperada sea verdadera, señalar la sincronización sin estado y luego desaparecer. El objeto sin estado no recuerda que fue señalizado (es sin estado). Entonces, el hilo original se queda dormido en el objeto de sincronización sin estado, y no se activa, aunque la condición que necesita se haya convertido en verdadera: despertador perdido.
Las funciones de espera de la variable de condición evitan el despertador perdido al asegurarse de que el hilo de llamada está registrado para detectar confiablemente el despertador antes de que abandone el mutex. Esto sería imposible si la función de espera de la variable de condición no tomara el mutex como argumento.
Parece ser una decisión de diseño específica en lugar de una necesidad conceptual.
Según los pthreads, la razón por la cual el mutex no se separó es que hay una mejora significativa en el rendimiento al combinarlos y esperan que, debido a las condiciones de carrera comunes, si no se usa un mutex, casi siempre se hará de todos modos.
https://linux.die.net/man/3/pthread_cond_wait
Características de mutexes y variables de condición
Se ha sugerido que la adquisición y liberación de mutex se desacople de la condición de espera. Esto fue rechazado porque es la naturaleza combinada de la operación lo que, de hecho, facilita las implementaciones en tiempo real. Esas implementaciones pueden mover de forma atómica un hilo de alta prioridad entre la variable de condición y el mutex de una manera que sea transparente para el que llama. Esto puede evitar interruptores de contexto adicionales y proporcionar una adquisición más determinista de un mutex cuando se señala el hilo de espera. Por lo tanto, la equidad y los problemas de prioridad se pueden tratar directamente mediante la disciplina de programación. Además, la operación de espera de condición actual coincide con la práctica existente.
Se supone que el mutex está bloqueado cuando llamas a pthread_cond_wait
; cuando lo llamas atómicamente, tanto desbloquea el mutex como luego bloquea la condición. Una vez que se señala la condición, la bloquea atómicamente de nuevo y regresa.
Esto permite la implementación de una programación predecible si se desea, ya que el hilo que estaría haciendo la señalización puede esperar hasta que se libere el mutex para hacer su procesamiento y luego señalar la condición.
Una variable de condición es bastante limitada si solo puede indicar una condición, generalmente necesita manejar algunos datos relacionados con la condición que se indicó. La señalización / activación tiene que hacerse atómicamente para lograr eso sin introducir condiciones de carrera, o ser demasiado complejo
pthreads también puede darte, por razones bastante técnicas, un despertar espurio . Eso significa que debe verificar un predicado, para estar seguro de que la condición fue realmente señalizada y distinguirlo de un despertar espurio. Es necesario vigilar tal condición con respecto a la espera, por lo que una variable de condición necesita una manera de esperar / despertar atómicamente mientras bloquea / desbloquea un mutex que guarda esa condición.
Considere un ejemplo simple en el que se le notifica que se producen algunos datos. Tal vez otro hilo hizo algunos datos que desea, y establecer un puntero a esa información.
Imagine un hilo de productor que proporciona datos a otro hilo de consumidor a través de un puntero ''some_data''.
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
char *data = some_data;
some_data = NULL;
handle(data);
}
naturalmente obtendrías muchas condiciones de carrera, ¿y si el otro subproceso hiciera some_data = new_data
justo después de que despertaras, pero antes de hacerlo data = some_data
Realmente no puedes crear tu propio mutex para proteger este caso, ya sea .eg
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
pthread_mutex_lock(&mutex);
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
No funcionará, todavía hay una posibilidad de una condición de carrera entre despertar y agarrar el mutex. Colocar el mutex antes del pthread_cond_wait no lo ayuda, ya que ahora mantendrá el mutex mientras espera, es decir, el productor nunca podrá tomar el mutex. (tenga en cuenta que, en este caso, podría crear una segunda variable de condición para some_data
al productor que ha finalizado con some_data
, aunque esto se volverá complejo, especialmente si desea muchos productores / consumidores).
Por lo tanto, necesita una forma de liberar / agarrar atómicamente el mutex al esperar / despertar de la condición. Eso es lo que hace pthread condition variables, y esto es lo que harías:
while(1) {
pthread_mutex_lock(&mutex);
while(some_data == NULL) { // predicate to acccount for spurious wakeups,would also
// make it robust if there were several consumers
pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
}
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
(el productor naturalmente necesitaría tomar las mismas precauciones, siempre guardando ''some_data'' con el mismo mutex, y asegurándose de que no sobreescriba some_data si some_data está actualmente! = NULL)