Mutex ejemplo / tutorial? [cerrado]

176

Soy nuevo en multihilo y estaba tratando de entender cómo funcionan los mutexes. Busqué mucho en Google, pero aún dejó algunas dudas sobre cómo funciona porque creé mi propio programa en el que el bloqueo no funcionaba.

Una sintaxis absolutamente no intuitiva del mutex es pthread_mutex_lock( &mutex1 );, donde parece que el mutex está bloqueado, cuando lo que realmente quiero bloquear es alguna otra variable. ¿Esta sintaxis significa que bloquear un mutex bloquea una región de código hasta que el mutex se desbloquea? Entonces, ¿cómo saben los hilos que la región está bloqueada? [ ACTUALIZACIÓN: los subprocesos saben que la región está bloqueada, por cercado de memoria ]. ¿Y no se supone que ese fenómeno se llama sección crítica? [ ACTUALIZACIÓN: Los objetos de sección crítica están disponibles solo en Windows, donde los objetos son más rápidos que los mutexes y son visibles solo para el hilo que lo implementa. De lo contrario, la sección crítica solo se refiere al área de código protegida por un mutex ]

En resumen, ¿podría ayudarnos con el programa de ejemplo mutex más simple posible y la explicación más simple posible sobre la lógica de cómo funciona? Estoy seguro de que esto ayudará a muchos otros novatos.

Nav
fuente
2
Continuar enfatizando la necesidad de un tutorial simple (ya sea impulsar hilos, tbb o pthreads): Ejemplos de la confusión: 1. stackoverflow.com/questions/3528877/… 2. stackoverflow.com/questions/2979525/… 3. stackoverflow.com/questions/2095977/to-mutex-or-not-to-mutex 4. stackoverflow.com/questions/3931026/… 5. stackoverflow.com/questions/1525189/…
Nav
1
No quiero decir esto ofensivamente, pero lo que su último comentario me sugiere es que necesitamos menos analogías y una mejor explicación técnica de cómo funciona un mutex y por qué los necesitamos.
San Jacinto
@San: Sin ofender :) Mis comentarios solo pretendían sugerir que un novato podría obtener la explicación más breve y clara de los mutexes. Muchas analogías pueden ser confusas para el novato, por lo que las diferentes analogías deben mantenerse por separado. Toda la razón por la que publiqué las preguntas y respuestas es porque, como novato, me resultaba difícil leer largas explicaciones y ejemplos de código. No quisiera que nadie más pasara por el dolor.
Nav
2
@Cory: Si esta respuesta pudiera mejorarse, me complacería recibir sus sugerencias. Estoy feliz de que muchas otras personas lo hayan encontrado útil. Si no le ayudó, también hay respuestas de otras personas que han señalado otros tutoriales de mutex. ¿Por qué ser tan negativo?
Nav

Respuestas:

278

Aquí va mi humilde intento de explicar el concepto a los novatos de todo el mundo: (una versión codificada por colores en mi blog también)

Muchas personas corren a una cabina telefónica solitaria (no tienen teléfonos móviles) para hablar con sus seres queridos. La primera persona que agarra la manija de la puerta de la cabina es la que tiene permitido usar el teléfono. Tiene que seguir agarrando la manija de la puerta mientras usa el teléfono, de lo contrario, alguien más agarrará la manija, lo arrojará y hablará con su esposa :) No hay un sistema de colas como tal. Cuando la persona termina su llamada, sale de la cabina y deja la manija de la puerta, la siguiente persona que la agarre podrá usar el teléfono.

Un hilo es: Cada persona
El mutex es: El tirador de la puerta
La cerradura es: La mano de la persona
El recurso es: El teléfono

Cualquier subproceso que tenga que ejecutar algunas líneas de código que no deben ser modificadas por otros subprocesos al mismo tiempo (usando el teléfono para hablar con su esposa), primero debe adquirir un candado en un mutex (agarrando la manija de la puerta de la cabina ) Solo entonces un hilo podrá ejecutar esas líneas de código (haciendo la llamada telefónica).

Una vez que el hilo ha ejecutado ese código, debe liberar el bloqueo en el mutex para que otro hilo pueda adquirir un bloqueo en el mutex (otras personas pueden acceder a la cabina del teléfono).

[ El concepto de tener un mutex es un poco absurdo cuando se considera el acceso exclusivo del mundo real, pero en el mundo de la programación, supongo que no había otra manera de permitir que los otros hilos 'vean' que un hilo ya estaba ejecutando algunas líneas de código. Hay conceptos de mutex recursivos, etc., pero este ejemplo solo pretendía mostrarle el concepto básico. Espero que el ejemplo te dé una idea clara del concepto. ]

Con roscado C ++ 11:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 
{
    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door
}

int main() 
{
    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;
}

Compila y ejecuta usando g++ -std=c++0x -pthread -o thread thread.cpp;./thread

En lugar de usar explícitamente locky unlock, puede usar corchetes como se muestra aquí , si está usando un bloqueo de alcance para la ventaja que proporciona . Sin embargo, las cerraduras con alcance tienen un ligero rendimiento general.

Nav
fuente
2
@San: Seré honesto; Sí, me gusta el hecho de que ha hecho todo lo posible para explicar los detalles (con flujo) a un novato completo. PERO, (por favor no me malinterpreten) la intención de esta publicación fue poner el concepto en una breve explicación (porque las otras respuestas apuntaban a largos tutoriales). Espero que no le importe si le pido que copie toda su respuesta y la publique como una respuesta separada. Para que pueda revertir y editar mi respuesta para señalar su respuesta.
Nav
2
@ Tom En ese caso, no deberías acceder a ese mutex. Las operaciones en él deben encapsularse de modo que todo lo que esté protegiendo esté protegido de tales tonterías. Si cuando utiliza la API expuesta de la biblioteca, se garantiza que la biblioteca es segura para subprocesos, entonces puede incluir un mutex claramente diferente para proteger sus propios elementos compartidos. De lo contrario, de hecho está agregando una nueva manija de la puerta, como ha sugerido.
San Jacinto
2
Para extender mi punto, lo que querría hacer es agregar otra habitación más grande alrededor de la cabina. La habitación también puede contener un baño y una ducha. Digamos que solo se permite la entrada de 1 persona a la vez. Debe diseñar la habitación para que esta habitación tenga una puerta con una manija que proteja la entrada a la habitación de manera muy similar a la cabina telefónica. Así que ahora, aunque tenga mutexes adicionales, puede reutilizar la cabina telefónica en cualquier proyecto. Otra opción sería exponer mecanismos de bloqueo para cada dispositivo en la sala y administrar las cerraduras en la clase de sala. De cualquier manera, no agregaría nuevos bloqueos al mismo objeto.
San Jacinto
8
Su ejemplo de subprocesamiento de C ++ 11 es incorrecto . Así es el TBB, la pista está en el bloqueo de ámbito de nombre .
Jonathan Wakely
3
Soy muy consciente de ambos, @ Jonathan. Parecía haber perdido la frase que escribí (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. En cuanto al uso del bloqueo de ámbito, depende del desarrollador, dependiendo del tipo de aplicación que estén creando. Esta respuesta tenía como objetivo abordar la comprensión básica del concepto mutex y no entrar en todas las complejidades del mismo, por lo que sus comentarios y enlaces son bienvenidos, pero están un poco fuera del alcance de este tutorial.
Nav
41

Si bien un mutex puede usarse para resolver otros problemas, la razón principal por la que existen es para proporcionar una exclusión mutua y, por lo tanto, resolver lo que se conoce como una condición de carrera. Cuando dos (o más) subprocesos o procesos intentan acceder a la misma variable al mismo tiempo, tenemos potencial para una condición de carrera. Considere el siguiente código

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}

Los aspectos internos de esta función se ven muy simples. Es solo una declaración. Sin embargo, un equivalente típico del lenguaje de seudoensamblaje podría ser:

load i from memory into a register
add 1 to i
store i back into memory

Debido a que todas las instrucciones equivalentes en lenguaje ensamblador son necesarias para realizar la operación de incremento en i, decimos que incrementar i es una operación no atómica. Una operación atómica es aquella que puede completarse en el hardware con la garantía de no ser interrumpida una vez que la ejecución de la instrucción ha comenzado. El incremento i consiste en una cadena de 3 instrucciones atómicas. En un sistema concurrente donde varios hilos llaman a la función, surgen problemas cuando un hilo lee o escribe en el momento equivocado. Imagina que tenemos dos subprocesos que se ejecutan simultáneamente y uno llama a la función inmediatamente después del otro. Digamos también que hemos inicializado a 0. También suponga que tenemos muchos registros y que los dos hilos están usando registros completamente diferentes, por lo que no habrá colisiones. El momento real de estos eventos puede ser:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

Lo que sucedió es que tenemos dos subprocesos que se incrementan simultáneamente, nuestra función se llama dos veces, pero el resultado es inconsistente con ese hecho. Parece que la función solo se llamó una vez. Esto se debe a que la atomicidad está "rota" en el nivel de la máquina, lo que significa que los hilos pueden interrumpirse entre sí o trabajar juntos en los momentos incorrectos.

Necesitamos un mecanismo para resolver esto. Necesitamos imponer algunos pedidos a las instrucciones anteriores. Un mecanismo común es bloquear todos los hilos excepto uno. Pthread mutex utiliza este mecanismo.

Cualquier subproceso que tenga que ejecutar algunas líneas de código que puedan modificar de forma insegura los valores compartidos por otros subprocesos al mismo tiempo (usando el teléfono para hablar con su esposa), primero debe hacerse bloquear un mutex. De esta manera, cualquier subproceso que requiera acceso a los datos compartidos debe pasar a través del bloqueo mutex. Solo entonces un hilo podrá ejecutar el código. Esta sección de código se llama una sección crítica.

Una vez que el hilo ha ejecutado la sección crítica, debe liberar el bloqueo en el mutex para que otro hilo pueda adquirir un bloqueo en el mutex.

El concepto de tener un mutex parece un poco extraño cuando se considera que los humanos buscan acceso exclusivo a objetos físicos reales, pero al programar, debemos ser intencionales. Los procesos y subprocesos concurrentes no tienen la educación social y cultural que nosotros tenemos, por lo que debemos obligarlos a compartir datos de manera agradable.

Técnicamente hablando, ¿cómo funciona un mutex? ¿No sufre las mismas condiciones de carrera que mencionamos anteriormente? ¿No es pthread_mutex_lock () un poco más complejo que un simple incremento de una variable?

Técnicamente hablando, necesitamos un poco de soporte de hardware para ayudarnos. Los diseñadores de hardware nos dan instrucciones de la máquina que hacen más de una cosa pero están garantizadas para ser atómicas. Un ejemplo clásico de tal instrucción es el test-and-set (TAS). Al tratar de adquirir un bloqueo en un recurso, podríamos usar el TAS para verificar si un valor en la memoria es 0. Si es así, esa sería nuestra señal de que el recurso está en uso y no hacemos nada (o más exactamente , esperamos por algún mecanismo. Un mutex pthreads nos pondrá en una cola especial en el sistema operativo y nos notificará cuando el recurso esté disponible. Los sistemas Dumber pueden requerir que hagamos un ciclo de giro cerrado, probando la condición una y otra vez) . Si el valor en la memoria no es 0, el TAS establece la ubicación en algo distinto de 0 sin utilizar ninguna otra instrucción. Eso' s como combinar dos instrucciones de ensamblaje en 1 para darnos atomicidad. Por lo tanto, probar y cambiar el valor (si el cambio es apropiado) no se puede interrumpir una vez que ha comenzado. Podemos construir mutexes sobre tal instrucción.

Nota: algunas secciones pueden parecer similares a una respuesta anterior. Acepté su invitación a editar, él prefería la forma original en que estaba, así que me quedo con lo que tenía, que está infundido con un poco de su palabrería.

San jacinto
fuente
1
Muchas gracias, San. He vinculado a su respuesta :) En realidad, tenía la intención de que tomara mi respuesta + su respuesta y la publicara como una respuesta separada, para mantener el flujo. Realmente no me importa si reutilizas alguna parte de mi respuesta. No estamos haciendo esto por nosotros mismos de todos modos.
Nav
13

El mejor tutorial de hilos que conozco está aquí:

https://computing.llnl.gov/tutorials/pthreads/

Me gusta que esté escrito sobre la API, en lugar de sobre una implementación particular, y ofrece algunos ejemplos simples y agradables para ayudarlo a comprender la sincronización.

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
Estoy de acuerdo en que definitivamente es un buen tutorial, pero es mucha información en una sola página y los programas son largos. La pregunta que publiqué es la versión mutex del discurso "Tengo un sueño", donde los novatos encontrarían una manera simple de aprender sobre mutexes y comprender cómo funciona la sintaxis no intuitiva (esta es una explicación que falta en todos los tutoriales) .
Nav
7

Me topé con esta publicación recientemente y creo que necesita una solución actualizada para el mutex c ++ 11 de la biblioteca estándar (es decir, std :: mutex).

Pegué un código a continuación (mis primeros pasos con un mutex: aprendí la concurrencia en win32 con HANDLE, SetEvent, WaitForMultipleObjects, etc.).

Como es mi primer intento con std :: mutex y sus amigos, ¡me encantaría ver comentarios, sugerencias y mejoras!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        {
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        }
    );


    std::thread thrProducer(
        [&]()
        {
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            {
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                {
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                }
            }
        }   
    );

    std::thread thrConsumer(
        [&]()
        {
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            {
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                {
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                }               
            }
        }
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;
}
comida para pez
fuente
1
¡Súper! Gracias por publicar. Aunque, como mencioné antes, mi propósito era simplemente explicar el concepto de un mutex. Todos los demás tutoriales lo hicieron muy difícil con los conceptos agregados de productor productor y variables de condición, etc., lo que me dificultó mucho entender lo que estaba pasando.
Nav
4

La función pthread_mutex_lock()o bien adquiere el mutex para el subproceso de llamada o bloquea el hilo hasta que el mutex puede ser adquirida. El relacionado pthread_mutex_unlock()lanza el mutex.

Piense en el mutex como una cola; cada hilo que intente adquirir el mutex se colocará al final de la cola. Cuando un subproceso libera el mutex, el siguiente subproceso de la cola se sale y ahora se está ejecutando.

Una sección crítica se refiere a una región de código donde es posible el no determinismo. A menudo esto se debe a que varios subprocesos intentan acceder a una variable compartida. La sección crítica no es segura hasta que se establezca algún tipo de sincronización. Un bloqueo de mutex es una forma de sincronización.

chrisaycock
fuente
1
¿Se garantiza que entrará exactamente el siguiente subproceso de intento?
Arsen Mkrtchyan
1
@Arsen Sin garantía. Es solo una analogía útil.
chrisaycock
3

Se supone que debe verificar la variable mutex antes de usar el área protegida por el mutex. Por lo tanto, su pthread_mutex_lock () podría (dependiendo de la implementación) esperar hasta que se libere mutex1 o devolver un valor que indique que no se pudo obtener el bloqueo si otra persona ya lo ha bloqueado.

Mutex es realmente solo un semáforo simplificado. Si lees sobre ellos y los entiendes, entiendes mutexes. Hay varias preguntas sobre mutexes y semáforos en SO. Diferencia entre semáforo binario y mutex , cuándo debemos usar mutex y cuándo debemos usar semáforo, etc. El ejemplo del inodoro en el primer enlace es tan bueno como uno puede pensar. Todo lo que hace el código es verificar si la clave está disponible y, si lo está, la reserva. Tenga en cuenta que realmente no reserva el inodoro en sí, sino la llave.

Makis
fuente
1
pthread_mutex_lockno puede regresar si alguien más tiene la cerradura. Se bloquea en este caso y ese es el punto. pthread_mutex_trylockes la función que volverá si se mantiene el bloqueo.
R .. GitHub DEJA DE AYUDAR AL HIELO
1
Sí, al principio no me di cuenta de qué implementación se trata.
Makis
3

Para aquellos que buscan el ejemplo shortex mutex:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}
Vacío
fuente