¿Por qué las funciones variables de condición de pthreads requieren un mutex?

182

Estoy leyendo pthread.h; Las funciones relacionadas con la variable de 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 usarlo como argumento? ¿Qué se supone que debe hacer ese mutex?

ELLIOTTCABLE
fuente

Respuestas:

194

Es solo la forma en que las variables de condición se implementan (o fueron originalmente).

El mutex se utiliza para proteger la variable de condición en sí . Por eso es necesario que lo bloquees antes de esperar.

La espera "atómicamente" desbloqueará el mutex, permitiendo que otros accedan a la variable de condición (para señalización). Luego, cuando se señala o se transmite la variable de condición, se despertará uno o más subprocesos en la lista de espera y el mutex se bloqueará mágicamente nuevamente para ese subproceso.

Por lo general, ve la siguiente operación con variables de condición, que ilustra cómo funcionan. El siguiente ejemplo es un subproceso de trabajo al que se le da trabajo mediante 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 algo disponible cuando regrese la espera. Cuando el hilo se ha marcado para dejar de funcionar (normalmente otro hilo configura la condición de salida y luego patea la variable de condición para activar este hilo), el bucle 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 múltiples consumidores, puede usar, como 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 que otros consumidores reciban trabajo mientras este está trabajando.

La variable de condición lo libera de la carga de sondear alguna condición, permitiendo que otro hilo le notifique cuando algo debe suceder. Otro hilo puede decir que ese 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 llama erróneamente despertares espurios generalmente se debe a que se han señalado múltiples hilos dentro de su pthread_cond_waitllamada (transmisió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 que hacer. Por lo tanto, tenía que tener una variable adicional que indicara que el trabajo debería hacerse (esto estaba inherentemente protegido por mutex con el par condvar / mutex aquí; sin embargo, se necesitaban otros hilos para bloquear el mutex antes de cambiarlo).

Que era técnicamente imposible que un hilo para volver a una condición de espera sin ser expulsado por otro proceso (esto es un verdadero despertar espuria), 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. Tal vez eso fue solo porque HP tuvo una implementación decente :-)

En cualquier caso, el mismo código que manejó el caso erróneo también manejó despertadores falsos genuinos, ya que el indicador de trabajo disponible no se establecería para esos.

paxdiablo
fuente
3
'hacer algo' no debería estar dentro del ciclo while. Desearía que su ciclo while solo verifique la condición, de lo contrario, también podría 'hacer algo' si recibe una vigilia espuria.
nos
1
no, el manejo de errores es lo segundo. Con pthreads, puede ser despertado, sin razón aparente (un despertar espurio) y sin ningún error. Por lo tanto, debe volver a verificar 'alguna condición' después de que se despierte.
nos
1
No estoy seguro de entender. Tuve la misma reacción que nos ; ¿Por qué está do somethingdentro del whilebucle?
ELLIOTTCABLE
1
Quizás no lo estoy aclarando lo suficiente. El ciclo es no esperar a que el trabajo esté listo para que pueda hacerlo. El bucle es el bucle de trabajo principal "infinito". Si regresa de cond_wait y se establece el indicador de trabajo, realiza el trabajo y luego vuelve a recorrerlo. "while some condition" solo será falso cuando desee que el hilo deje de funcionar en ese momento, liberará el mutex y muy probablemente saldrá.
paxdiablo
77
@stefaanv "el mutex aún debe proteger la variable de condición, no hay otra forma de protegerlo": el mutex no debe proteger la variable de condición; es para proteger los datos del predicado , pero creo que lo sabes al leer tu comentario que siguió a esa declaración. Puede señalar una variable de condición legalmente, y totalmente compatible con implementaciones, después del desbloqueo del mutex que envuelve el predicado, y de hecho aliviará la contención al hacerlo en algunos casos.
WhozCraig
59

Una variable de condición es bastante limitada si solo puede señalar una condición, por lo general, necesita manejar algunos datos relacionados con la condición que se señaló. La señalización / activación debe hacerse atómicamente para lograr eso sin introducir condiciones de carrera, o ser demasiado complejo

pthreads también puede proporcionarle, por razones bastante técnicas, una activación espuria . Eso significa que debe verificar un predicado, de modo que pueda estar seguro de que la condición realmente se señaló, y distinguirlo de una vigilia espuria. La verificación de dicha condición en lo que respecta a esperar debe ser protegida, por lo que una variable de condición necesita una forma de esperar / despertar atómicamente mientras se bloquea / desbloquea un mutex que protege esa condición.

Considere un ejemplo simple donde se le notifica que se producen algunos datos. Tal vez otro hilo creó algunos datos que desea y estableció un puntero a esos datos.

Imagine un hilo productor que da algunos datos a otro hilo 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, ¿qué pasaría si el otro hilo lo hiciera some_data = new_datajusto después de que te despertaran, pero antes de que lo hicieras?data = some_data

Realmente tampoco puedes crear tu propio mutex para proteger este caso .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 despertarse y agarrar el mutex. Colocar el mutex antes de 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 indicarle al productor que ha terminado 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 cuando espera / se despierta de la condición. Eso es lo que hace las variables de condición pthread, y esto es lo que haría:

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 protegiendo 'some_data' con el mismo mutex, y asegurándose de que no sobrescriba some_data si some_data está actualmente! = NULL)

nos
fuente
¿No debería while (some_data != NULL)ser un ciclo do-while para que espere la variable de condición al menos una vez?
Juez Maygarden
3
No. Lo que realmente estás esperando es que 'some_data' no sea nulo. Si no es nulo la "primera vez", genial, tiene el mutex y puede usar los datos de forma segura. Si tuviera un ciclo do / while, se perdería la notificación si alguien señalara la variable de condición antes de esperarla (no se parece en nada a los eventos encontrados en win32 que permanecen señalados hasta que alguien los espera)
nos
44
Acabo de tropezar con esta pregunta y, francamente, es extraño encontrar que esta respuesta, que es correcta, tiene muchos menos puntos que la respuesta de paxdiablo que tiene fallas definidas (todavía se necesita atomicidad, el mutex solo se necesita para manejar la condición, no para manipular o notificar). Supongo que así es como funciona stackoverflow ...
stefaanv
@stefaanv, si desea detallar los defectos, como comentarios a mi respuesta para que los vea de manera oportuna, en lugar de meses después :-), estaré encantado de solucionarlos. Tus breves frases realmente no me dan suficientes detalles para entender lo que estás tratando de decir.
paxdiablo
1
@nos, no debería while(some_data != NULL)ser while(some_data == NULL)?
Eric Z
30

Las variables de condición POSIX no tienen estado. Por lo tanto, es su responsabilidad mantener el estado. Dado que tanto los subprocesos que esperan como los subprocesos que le dicen a otros subprocesos que dejen de esperar, deben estar protegidos por un mutex. Si cree que puede usar variables de condición sin un mutex, entonces no ha comprendido que las variables de condición no tienen estado.

Las variables de condición se construyen alrededor de una condición. Los subprocesos que esperan en una variable de condición están esperando alguna condición. Los hilos que indican que las variables de condición cambian esa condición. Por ejemplo, un hilo podría 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, simplificado:

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 espera trabajo. El trabajo está protegido por un mutex. La espera libera el mutex para que otro hilo pueda darle algo 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 con 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.

David Schwartz
fuente
1
O, para decirlo de manera más concisa, el punto completo de las variables de condición es proporcionar una operación atómica de "desbloqueo y espera". Sin un mutex, no habría nada que desbloquear.
David Schwartz
¿Te importaría explicar el significado de apátrida ?
snr
@snr No tienen ningún estado. No están "bloqueados" o "señalizados" o "sin señalizar". Por lo tanto, es su responsabilidad realizar un seguimiento de cualquier estado asociado con la variable de condición. Por ejemplo, si la variable de condición le permite a un subproceso saber cuándo una cola deja de estar vacía, debe darse el caso de que un subproceso puede hacer que la cola no esté vacía y algún otro subproceso necesita saber cuándo la cola deja de estar vacía. Ese es un estado compartido y debe protegerlo con un mutex. Puede usar la variable de condición, en asociación con ese estado compartido protegido por un mutex, como mecanismo de activación.
David Schwartz
16

No todas las funciones variables de condición requieren un mutex: solo las operaciones de espera. Las operaciones de señal y transmisión no requieren mutex. Una variable de condición tampoco está asociada permanentemente 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 subprocesos en espera, esto debe estar protegido 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 subproceso ha bloqueado el mutex, evaluó alguna expresión sobre las variables compartidas y descubrió que es falso, por lo que debe esperar.
  • el subproceso debe pasar atómicamente de ser propietario del mutex a esperar 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 subproceso desde poseer el mutex hasta esperar, para que el subproceso 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 una ventana de tiempo cuando el subproceso ya no tiene el bloqueo y tiene Aún no ha comenzado a esperar el objeto. Durante esta ventana, puede entrar otro subproceso, 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 se haya señalado (no tiene estado). Entonces, el subproceso original se duerme en el objeto de sincronización sin estado y no se activa, a pesar de que la condición que necesita ya se ha convertido en realidad: la activación perdida.

Las funciones de espera de condición variable evitan la activación perdida asegurándose de que el hilo de llamada esté registrado para capturar de manera confiable la activación 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.

Kaz
fuente
¿Podría proporcionar una referencia de que las operaciones de difusión no requieren adquirir el mutex? En MSVC, la transmisión se ignora.
xvan
@xvan El POSIX pthread_cond_broadcasty las pthread_cond_signaloperaciones (de las que trata esta pregunta SO) ni siquiera toman el mutex como argumento; Solo la condición. La especificación POSIX está aquí . El mutex solo se menciona en referencia a lo que sucede en los hilos de espera cuando se despiertan.
Kaz
¿Te importaría explicar el significado de apátrida ?
snr
1
@snr Un objeto de sincronización sin estado no recuerda ningún estado relacionado con la señalización. Cuando se señala, si algo lo espera ahora, se despierta, de lo contrario se olvida el despertar. Las variables de condición no tienen estado de esta manera. La aplicación mantiene el estado necesario para hacer que la sincronización sea confiable y la protege el mutex que se usa junto con las variables de condición, de acuerdo con la lógica escrita correctamente.
Kaz
7

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 envolverlo wait()en un mutex:

  1. sin un mutex otro hilo podría signal()antes wait()y nos perderíamos este despertar.
  2. normalmente check()depende de la modificación de otro hilo, por lo que necesita exclusión mutua de todos modos.
  3. para asegurarse de que el subproceso de mayor prioridad proceda primero (la cola para el mutex permite que el planificador decida quién sigue).

El tercer punto no siempre es preocupante: el contexto histórico está vinculado desde el artículo a esta conversación .

A menudo se mencionan espurias despertares con respecto a este mecanismo (es decir, el hilo de espera se despierta sin signal()ser llamado). Sin embargo, tales eventos son manejados por el bucle check().

Sam Brightman
fuente
4

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 subproceso que indique la variable de condición, por lo que el subproceso 1 esperará para siempre, a pesar de que protectedReadyToRunVariable dice que está listo.

La única forma de evitar esto es que las variables de condición liberen atómicamente el mutex mientras simultáneamente comienzan a esperar en la variable de condición. Es por eso que 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);
Cort Ammon
fuente
3

Se supone que el mutex está bloqueado cuando llamas pthread_cond_wait; cuando lo llamas atómicamente, desbloquea el mutex y 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.

Ámbar
fuente
Entonces ... ¿hay alguna razón para que no solo deje el mutex siempre desbloqueado, y luego lo bloquee justo antes de esperar, y luego lo desbloquee justo después de que termine la espera?
ELLIOTTCABLE
El mutex también resuelve algunas carreras potenciales entre los hilos de espera y señalización. siempre y cuando el mutex esté siempre bloqueado al cambiar la condición y la señalización, nunca se encontrará perdiendo la señal y durmiendo para siempre
Hasturkun
Entonces ... ¿ primero debo esperar en mutex en el mutex conditionvar, antes de esperar en el conditionvar? No estoy seguro de entenderlo en absoluto.
ELLIOTTCABLE
2
@elliottcable: Sin sostener el mutex, ¿cómo podría saber si debe o no esperar? ¿Qué pasa si lo que estás esperando acaba de suceder?
David Schwartz
1

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);
}
Unidad central de pensamiento
fuente
1

Parece ser una decisión de diseño específica más que una necesidad conceptual.

Según los documentos de pthreads, la razón por la que el mutex no se separó es porque hay una mejora significativa en el rendimiento al combinarlos y esperan que, debido a las condiciones de carrera comunes, si no usas 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 sugirió 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 que, de hecho, facilita las implementaciones en tiempo real. Esas implementaciones pueden mover atómicamente un subproceso de alta prioridad entre la variable de condición y el mutex de manera transparente para la persona que llama. Esto puede evitar cambios adicionales de contexto y proporcionar una adquisición más determinista de un mutex cuando se señala el hilo de espera. Por lo tanto, los problemas de equidad y prioridad pueden ser tratados directamente por la disciplina de programación. Además, la operación de espera de la condición actual coincide con la práctica existente.

Catskul
fuente
0

Hay un montón de exégesis sobre eso, pero quiero resumirlo con un ejemplo a continuación.

1 void thr_child() {
2    done = 1;
3    pthread_cond_signal(&c);
4 }

5 void thr_parent() {
6    if (done == 0)
7        pthread_cond_wait(&c);
8 }

¿Qué hay de malo con el fragmento de código? Solo reflexiona un poco antes de seguir adelante.


El problema es realmente sutil. Si el padre invoca thr_parent()y luego examina el valor de done, verá que lo es 0y, por lo tanto, intentará conciliar el sueño. Pero justo antes de llamar a esperar para ir a dormir, el padre se interrumpe entre las líneas de 6-7 y el niño corre. El elemento secundario cambia la variable de estado donea 1señales, pero no hay ningún hilo esperando y, por lo tanto, no se despierta ningún hilo. Cuando el padre corre de nuevo, duerme para siempre, lo cual es realmente atroz.

¿Qué pasa si se llevan a cabo mientras se adquieren las cerraduras individualmente?

snr
fuente