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

Resuelto ELLIOTTCABLE asked hace 14 años • 10 respuestas

Estoy leyendo sobre 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 avatar May 04 '10 15:05 ELLIOTTCABLE
Aceptado

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

El mutex se utiliza para proteger la propia variable de condición. . Es por eso que necesitas bloquearlo antes de esperar.

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

Normalmente verá la siguiente operación con variables de condición, que ilustran cómo funcionan. El siguiente ejemplo es un subproceso de trabajo al que se le asigna 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 bucle siempre que haya algo disponible cuando regrese la espera. Cuando se ha marcado el hilo para que deje de funcionar (generalmente cuando otro hilo establece la condición de salida y luego activa 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 multiconsumidor, puede utilizar, 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 éste realiza el trabajo.

La variable de condición le libera de la carga de sondear alguna condición, en lugar de permitir que otro hilo le notifique cuando es necesario que suceda algo. Otro hilo puede indicarle que el trabajo 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 despertares espurios generalmente se debían a que se habían señalado varios subprocesos dentro de su pthread_cond_waitllamada (difusión), uno regresaba con el mutex, hacía el trabajo y luego volvía a esperar.

Entonces el segundo hilo señalado podría salir cuando ya no había trabajo que hacer. Por lo tanto, tenía que tener una variable adicional que indicara que se debía realizar el trabajo (esto estaba inherentemente protegido con mutex con el par condvar/mutex aquí; sin embargo, se necesitaban otros subprocesos para bloquear el mutex antes de cambiarlo).

Era técnicamente posible que un hilo regresara de una condición de espera sin ser expulsado por otro proceso (esto es un despertar genuino y espurio) pero, en todos mis muchos años trabajando en pthreads, tanto en desarrollo/servicio del código como 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 manejó el caso erróneo también manejó activaciones falsas genuinas, ya que la bandera de trabajo disponible no se establecería para ellas.

paxdiablo avatar May 04 '2010 08:05 paxdiablo

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/despertar debe realizarse de forma atómica para lograrlo sin introducir condiciones de carrera o ser demasiado complejo.

pthreads también puede proporcionarle, por razones bastante técnicas, una activación falsa . Eso significa que debe verificar un predicado, para poder estar seguro de que la condición realmente fue señalada y distinguirla de una activación espuria. La verificación de dicha condición con respecto a la espera debe estar protegida, por lo que una variable de condición necesita una forma de esperar/activarse atómicamente mientras se bloquea/desbloquea un mutex que protege esa condición.

Considere un ejemplo sencillo en el que se le notifica que se han producido algunos datos. Tal vez otro hilo creó algunos datos que desea y estableció un puntero a esos datos.

Imagine un hilo productor que proporciona 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 despertaras, pero antes de que lo hicieras?data = some_data

Realmente tampoco puedes crear tu propio mutex para proteger este caso.

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 existe la posibilidad de que se produzca una condición de carrera entre el despertar y la toma del mutex. Colocar el mutex antes de pthread_cond_wait no le 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 ya terminó 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/desperta de la condición. Eso es lo que hacen las variables de condición de pthread, 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);
}

(Naturalmente, el productor necesitaría tomar las mismas precauciones, protegiendo siempre 'algunos_datos' con el mismo mutex y asegurándose de que no sobrescriba algunos_datos si algunos_datos están actualmente! = NULL)

nos avatar May 04 '2010 09:05 nos

Las variables de condición POSIX no tienen estado. Así que es vuestra responsabilidad mantener el Estado. Dado que tanto los subprocesos que esperan como los subprocesos que le dicen a otros subprocesos que dejen de esperar accederán al estado, debe estar protegido por un mutex. Si cree que puede utilizar 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 una variable de condición están esperando alguna condición. Los subprocesos que señalan 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 han llegado los datos. "Los datos han llegado" es la condición.

Este es 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 está esperando trabajo. El trabajo está protegido por un mutex. La espera libera el mutex para que otro hilo pueda darle 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 trabajos. Observe 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 avatar Aug 27 '2011 12:08 David Schwartz