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.