pthread lock example ejemplos c++ c multithreading synchronization mutex

c++ - lock - ¿Ejemplo/tutorial Mutex?



pthread mutex (7)

EJEMPLO DE SEMÁFORO ::

sem_t m; sem_init(&m, 0, 0); // initialize semaphore to 0 sem_wait(&m); // critical section here sem_post(&m);

Referencia: http://pages.cs.wisc.edu/~remzi/Classes/537/Fall2008/Notes/threads-semaphores.txt

Soy nuevo en multihilo e intentaba entender cómo funcionan los mutexes. Hicimos un montón de Google y encontré un tutorial decente , pero aún dejó algunas dudas de 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á siendo bloqueado, cuando lo que realmente quiero bloquear es alguna otra variable. ¿Esta sintaxis significa que el bloqueo de un mutex bloquea una región de código hasta que se desbloquee el mutex? Entonces, ¿cómo saben los hilos que la región está bloqueada? [ ACTUALIZACIÓN: los hilos saben que la región está bloqueada, mediante Cercas 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 mutex y solo son visibles para el hilo que lo implementa. De lo contrario, la sección crítica 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.


El mejor tutorial sobre 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 en particular, y ofrece algunos buenos ejemplos simples para ayudarlo a comprender la sincronización.


La función pthread_mutex_lock() adquiere el mutex para la cadena que llama o bloquea la cadena hasta que se pueda adquirir la exclusión mutua. El pthread_mutex_unlock() relacionado libera 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 hilo libera el mutex, el siguiente hilo de la cola sale y se está ejecutando.

Una sección crítica se refiere a una región de código donde no es posible el determinismo. A menudo esto porque múltiples hilos intentan acceder a una variable compartida. La sección crítica no es segura hasta que haya algún tipo de sincronización en su lugar. Un bloqueo mutex es una forma de sincronización.


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

He pegado algunos códigos a continuación (mis primeros pasos con un mutex: aprendí concurrencia en win32 con HANDLE, SetEvent, WaitForMultipleObjects, etc.).

Ya que es mi primer intento con std :: mutex y 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; }


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

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

Las partes internas de esta función se ven tan simples. Es solo una declaración. Sin embargo, un equivalente de lenguaje de pseudoensamblaje típico podría ser:

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

Debido a que las instrucciones equivalentes en lenguaje de ensamblaje son todas necesarias para realizar la operación de incremento en i, decimos que incrementar i es una operación no atmoica. Una operación atómica es aquella que puede completarse en el hardware con la garantía de que no se interrumpirá una vez que la ejecución de la instrucción haya comenzado. Incrementando i consiste en una cadena de 3 instrucciones atómicas. En un sistema concurrente donde varios subprocesos llaman a la función, surgen problemas cuando un hilo lee o escribe en el momento equivocado. Imagine que tenemos dos hilos ejecutándose simultáneamente y uno llama a la función inmediatamente después de la otra. Digamos también que hemos inicializado a 0. Supongamos también que tenemos muchos registros y que los dos hilos están usando registros completamente diferentes, por lo que no habrá colisiones. El tiempo 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 hilos que aumentan al mismo tiempo, nuestra función recibe dos llamadas, 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 o trabajar juntos en el momento equivocado.

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

Cualquier hilo que tenga que ejecutar algunas líneas de código que pueden modificar de forma insegura los valores compartidos por otros hilos al mismo tiempo (usando el teléfono para hablar con su esposa), primero debe hacerse adquirir un bloqueo en un mutex. De esta forma, cualquier hilo que requiera acceso a los datos compartidos debe pasar por el bloqueo mutex. Solo entonces un hilo podrá ejecutar el código. Esta sección de código se llama 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 a los humanos que buscan acceso exclusivo a objetos físicos reales, pero cuando se programa, debemos ser intencionales. Los hilos y procesos concurrentes no tienen la educación social y cultural que nosotros hacemos, por lo que debemos obligarlos a compartir datos muy bien.

Entonces, 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 garantizados para ser atómicos. Un ejemplo clásico de tal instrucción es el conjunto de prueba y ajuste (TAS). Al tratar de obtener un bloqueo en un recurso, podemos usar el TAS para verificar si un valor en la memoria es 0. Si lo es, 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 pthreads mutex nos pondrá en una cola especial en el sistema operativo y nos notificará cuando el recurso esté disponible. Los sistemas más voluminosos 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. Es 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 además de tal instrucción.

Nota: algunas secciones pueden parecer similares a una respuesta anterior. Acepté su invitación para editar, prefirió la forma original, así que guardo lo que tenía impreso con un poco de su verborrea.


Se supone que debes 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 alguien más 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 deberíamos usar mutex y cuándo deberíamos usar semáforos y demás? El ejemplo de baño en el primer enlace es un buen ejemplo de lo que uno puede pensar. Todo lo que hace el código es verificar si la clave está disponible y si lo está, lo reserva. Tenga en cuenta que realmente no se reserva el inodoro, sino la llave.


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

Mucha gente corre a una cabina telefónica solitaria (sin teléfonos móviles) para hablar con sus seres queridos. La primera persona que atrapa la manija de la puerta del stand es quien puede usar el teléfono. Él tiene que seguir agarrándose a la manija de la puerta mientras use el teléfono, de lo contrario alguien agarrará el asa, lo arrojará y hablará con su esposa :) No hay un sistema de cola como tal. Cuando la persona termina su llamada, sale de la cabina y deja la manija de la puerta, la siguiente persona que agarre la manija de la puerta podrá usar el teléfono.

Un hilo es: cada persona
El mutex es: la manija de la puerta
El candado es: la mano de la persona
El recurso es: El teléfono

Cualquier hilo que tenga que ejecutar algunas líneas de código que no deberían ser modificadas por otros hilos al mismo tiempo (usando el teléfono para hablar con su esposa), primero debe adquirir un bloqueo en un mutex (agarrando la manija de la puerta del stand ) 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 puedan acceder a la cabina telefónica).

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

Con el enhebrado 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; }

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

En lugar de utilizar de forma explícita el lock y unlock , puede usar corchetes como se muestra aquí , si está utilizando un bloqueo de ámbito para la ventaja que proporciona . Sin embargo, los bloqueos de alcance tienen una ligera sobrecarga de rendimiento.

Con TBB: Necesitarás TBB para ejecutar el siguiente programa, pero la intención de publicar el código TBB es que entiendes la secuencia de bloqueo y desbloqueo simplemente mirando el código simple (podría haber mostrado bloqueo con alcance al no usar adquirir y lanzamiento, que también es una excepción segura , pero esto es más claro).

#include <iostream> #include "/tbb/mutex.h" #include "/tbb/tbb_thread.h" using namespace tbb; typedef mutex myMutex; static myMutex sm; int i = 0; void someFunction() { //Note: Since a scoped lock is used below, you should know that you //can specify a scope for the mutex using curly brackets, instead of //using lock.acquire() and lock.release(). The lock will automatically //get released when program control goes beyond the scope. myMutex::scoped_lock lock;//create a lock lock.acquire(sm);//Method acquire waits until it can acquire a lock on the mutex //***only one thread can access the lines from here...*** ++i;//incrementing i is safe (only one thread can execute the code in this scope) because the mutex locked above protects all lines of code until the lock release. sleep(1);//simply creating a delay to show that no other thread can increment i until release() is executed std::cout<<"In someFunction "<<i<<"/n"; //***...to here*** lock.release();//releases the lock (duh!) } int main() { tbb_thread my_thread1(someFunction);//create a thread which executes ''someFunction'' tbb_thread my_thread2(someFunction); tbb_thread my_thread3(someFunction); my_thread1.join();//This command causes the main thread (which is the ''calling-thread'' in this case) to wait until thread1 completes its task. my_thread2.join(); my_thread3.join(); }

Tenga en cuenta que tbb_thread.h está en desuso. El reemplazo se muestra here .