Por fin luego de tanto batallar hemos hallado el resultado de este dilema que ciertos usuarios de nuestro sitio presentan. Si tienes alguna información que compartir puedes aportar tu comentario.
Solución:
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)
Mucha gente corre a una cabina telefónica solitaria (no tienen teléfonos móviles) para hablar con sus seres queridos. La primera persona en agarrar la manija de la puerta de la cabina es la que tiene permitido usar el teléfono. Tiene que seguir agarrado a la manija de la puerta mientras use el teléfono, de lo contrario alguien más lo agarrará, lo echará y hablará con su esposa 🙂 No hay un sistema de colas como tal. Cuando la persona termine su llamada, salga de la cabina y deje la manija de la puerta, la próxima persona que tome la manija de la puerta podrá usar el teléfono.
A hilo es: cada persona
los mutex es: la manija de la puerta
los cerrar con llave es: la mano de la persona
los recurso es: el teléfono
Cualquier hilo que tenga que ejecutar algunas líneas de código que no deben ser modificadas por otros hilos 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 puedan acceder a la cabina telefónica).
[The concept of having a mutex is a bit absurd when considering real-world exclusive access, but in the programming world I guess there was no other way to let the other threads ‘see’ that a thread was already executing some lines of code. There are concepts of recursive mutexes etc, but this example was only meant to show you the basic concept. Hope the example gives you a clear picture of the concept.]
Con subprocesos de C ++ 11:
#include
#include
#include
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 usar explícitamente lock
y unlock
, puede usar corchetes como se muestra aquí, si está usando un candado con alcance para la ventaja que proporciona. Sin embargo, las cerraduras con alcance tienen una ligera sobrecarga de rendimiento.
Si bien un mutex puede usarse para resolver otros problemas, la razón principal por la que existen es para proporcionar exclusión mutua y, por lo tanto, resolver lo que se conoce como condición de carrera. Cuando dos (o más) subprocesos o procesos intentan acceder a la misma variable al mismo tiempo, existe la posibilidad de que se produzca 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 parecen tan simples. Es solo una declaración. Sin embargo, un equivalente típico en lenguaje pseudo-ensamblador podría ser:
load i from memory into a register
add 1 to i
store i back into memory
Debido a que se requieren todas las instrucciones equivalentes en lenguaje ensamblador 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 se puede completar en el hardware con la garantía de que no se interrumpirá una vez que haya comenzado la ejecución de la instrucción. Incrementar i consiste en una cadena de 3 instrucciones atómicas. En un sistema concurrente donde varios subprocesos están llamando a la función, surgen problemas cuando un subproceso lee o escribe en el momento equivocado. Imagine que tenemos dos subprocesos ejecutándose simultáneamente y uno llama a la función inmediatamente después del otro. Digamos también que tenemos i inicializado a 0. Supongamos también que tenemos muchos registros y que los dos subprocesos 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 aumentan i al mismo tiempo, 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 hilo que tenga que ejecutar algunas líneas de código que puedan modificar de manera insegura los valores compartidos por otros hilos al mismo tiempo (usando el teléfono para hablar con su esposa), primero debe 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 denomina sección crítica.
Una vez que el subproceso ha ejecutado la sección crítica, debe liberar el bloqueo en el mutex para que otro subproceso 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 hilos y procesos concurrentes no tienen la educación social y cultural que nosotros tenemos, por lo que debemos obligarlos a compartir datos de manera agradable.
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 algo 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 intentar adquirir un bloqueo en un recurso, podríamos 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 mutex de pthreads nos colocará en una cola especial en el sistema operativo y nos notificará cuando el recurso esté disponible. Los sistemas más tontos pueden requerir que hagamos un ciclo de giro ajustado, 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 diferente a 0 sin usar ninguna otra instrucción. Es como combinar dos instrucciones de ensamblaje en 1 para darnos atomicidad. Por lo tanto, la prueba y el cambio del valor (si el cambio es apropiado) no se puede interrumpir una vez que ha comenzado. Podemos construir mutex sobre tal instrucción.
Nota: algunas secciones pueden parecer similares a una respuesta anterior. Acepté su invitación para editar, él prefería la forma original, así que me quedo con lo que tenía, que está impregnado de un poco de su verborrea.
El mejor tutorial de subprocesos 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 brinda algunos ejemplos simples y agradables para ayudarlo a comprender la sincronización.