Mutex ( del inglés mutex , de exclusión mutua - “exclusión mutua”) es una primitiva de sincronización que proporciona exclusión mutua de la ejecución de secciones críticas de código [1] . Un mutex clásico se diferencia de un semáforo binario por la presencia de un propietario exclusivo, que debe liberarlo (es decir, transferirlo a un estado desbloqueado) [2] . Un mutex se diferencia de un spinlock en que pasa el control al programador para cambiar los subprocesos cuando no se puede adquirir el mutex [3] . También hay bloqueos de lectura y escritura., llamados mutex compartidos, que proporcionan, además del bloqueo exclusivo, un bloqueo compartido que permite la propiedad compartida del mutex si no hay un propietario exclusivo [4] .
Convencionalmente, un mutex clásico se puede representar como una variable que puede estar en dos estados: bloqueado y desbloqueado. Cuando un subproceso ingresa a su sección crítica, llama a una función para bloquear la exclusión mutua, bloqueando el subproceso hasta que se libera la exclusión mutua si otro subproceso ya lo posee. Al salir de la sección crítica, el subproceso llama a la función para mover el mutex al estado desbloqueado. Si hay varios subprocesos bloqueados por un mutex durante el desbloqueo, el planificador selecciona un subproceso para reanudar la ejecución (dependiendo de la implementación, puede ser un subproceso aleatorio o un subproceso determinado por algún criterio) [5] .
El trabajo de un mutex es proteger el objeto de ser accedido por otros subprocesos que no sean el propietario del mutex. En un momento dado, solo un subproceso puede poseer un objeto protegido por un mutex. Si otro subproceso necesita acceso a los datos protegidos por la exclusión mutua, ese subproceso se bloqueará hasta que se libere la exclusión mutua. Un mutex protege los datos para que no se corrompan por cambios asincrónicos ( una condición de carrera ), pero se pueden causar otros problemas como interbloqueo o captura doble si se usa incorrectamente.
Por tipo de implementación, el mutex puede ser rápido, recursivoo con control de errores.
Una inversión de prioridad ocurre cuando un proceso de alta prioridad debería estar ejecutándose, pero bloquea un mutex propiedad del proceso de baja prioridad y debe esperar hasta que el proceso de baja prioridad desbloquee el mutex. Un ejemplo clásico de inversión de prioridad sin restricciones en sistemas en tiempo real es cuando un proceso con una prioridad media aprovecha el tiempo de la CPU, como resultado de lo cual el proceso con una prioridad baja no puede ejecutarse y no puede desbloquear el mutex [6] .
Una solución típica al problema es la herencia de prioridad, en la que un proceso que posee un mutex hereda la prioridad de otro proceso bloqueado por él, si la prioridad del proceso bloqueado es mayor que la del actual [6] .
La API de Win32 en Windows tiene dos implementaciones de mutexes: los propios mutexes, que tienen nombres y están disponibles para su uso entre diferentes procesos [7] , y las secciones críticas , que solo pueden ser utilizadas dentro del mismo proceso por diferentes subprocesos [8] . Cada uno de estos dos tipos de mutexes tiene sus propias funciones de captura y liberación [9] . La sección crítica en Windows es un poco más rápida y eficiente que el mutex y el semáforo porque utiliza la instrucción de prueba y configuración específica del procesador [8] .
El paquete Pthreads proporciona varias funciones que se pueden usar para sincronizar hilos [10] . Entre estas funciones hay funciones para trabajar con mutexes. Además de las funciones de adquisición y liberación de exclusión mutua, se proporciona una función de intento de adquisición de exclusión mutua que devuelve un error si se espera un bloqueo de subprocesos. Esta función se puede utilizar en un bucle de espera activo si surge la necesidad [11] .
Funciones del paquete pthreads para trabajar con mutexesFunción | Descripción |
---|---|
pthread_mutex_init() | Crear un mutex [11] . |
pthread_mutex_destroy() | Destrucción de exclusión mutua [11] . |
pthread_mutex_lock() | Transferencia de un mutex a un estado bloqueado (captura de mutex) [11] . |
pthread_mutex_trylock() | Intente poner el mutex en el estado bloqueado y devuelva un error si el subproceso debe bloquearse porque el mutex ya tiene un propietario [11] . |
pthread_mutex_timedlock() | Intente mover el mutex al estado bloqueado y devuelva un error si el intento falló antes del tiempo especificado [12] . |
pthread_mutex_unlock() | Transferir el mutex al estado desbloqueado (liberación del mutex) [11] . |
Para resolver problemas especializados, a los mutex se les pueden asignar varios atributos [11] . A través de los atributos, utilizando la función pthread_mutexattr_settype(), puede establecer el tipo de mutex, lo que afectará el comportamiento de las funciones para capturar y liberar el mutex [13] . Un mutex puede ser uno de tres tipos [13] :
El estándar C17 del lenguaje de programación C define un tipo mtx_t[15] y un conjunto de funciones para trabajar con él [16] que deben estar disponibles si la macro __STDC_NO_THREADS__no ha sido definida por el compilador [15] . La semántica y las propiedades de los mutex son generalmente consistentes con el estándar POSIX.
El tipo de mutex se determina pasando una combinación de banderas a la función mtx_init()[17] :
La posibilidad de utilizar mutexes a través de memoria compartida por diferentes procesos no está contemplada en el estándar C17.
El estándar C++17 del lenguaje de programación C++ define 6 clases mutex diferentes [20] :
La biblioteca Boost también proporciona exclusiones mutuas con nombre y entre procesos, así como exclusiones mutuas compartidas, que permiten la adquisición de una exclusión mutua para la propiedad compartida por múltiples subprocesos de datos de solo lectura sin exclusión de escritura durante la adquisición del bloqueo, que es esencialmente un mecanismo para bloqueos de lectura y escritura [25] .
En el caso general, el mutex almacena no solo su estado, sino también una lista de tareas bloqueadas. El cambio de estado de una exclusión mutua se puede implementar mediante operaciones atómicas dependientes de la arquitectura en el nivel de código de usuario, pero al desbloquear la exclusión mutua, también se deben reanudar otras tareas que fueron bloqueadas por la exclusión mutua. Para estos fines, una primitiva de sincronización de nivel inferior es muy adecuada: futex , que se implementa en el lado del sistema operativo y asume la funcionalidad de bloquear y desbloquear tareas, lo que permite, entre otras cosas, crear mutex entre procesos [26] . En particular, usando el futex, el mutex se implementa en el paquete Pthreads en muchas distribuciones de Linux [27] .
La simplicidad de los mutex les permite implementarse en el espacio del usuario mediante una instrucción de ensamblador XCHGque puede copiar atómicamente el valor del mutex en un registro y, al mismo tiempo, establecer el valor del mutex en 1 (previamente escrito en el mismo registro). Un valor mutex de cero significa que está en estado bloqueado, mientras que un valor de uno significa que está en estado desbloqueado. El valor del registro se puede probar para 0, y en el caso de un valor cero, el control debe devolverse al programa, lo que significa que se adquiere el mutex, si el valor no era cero, entonces el control debe transferirse a el planificador para reanudar el trabajo de otro subproceso, seguido de un segundo intento de adquirir el mutex, que sirve como un análogo del bloqueo activo. Un mutex se desbloquea almacenando el valor 0 en el mutex usando el comando XCHG[28] . Alternativamente, se puede usar LOCK BTS(implementación TSL para un bit) o CMPXCHG[29] ( implementación CAS ).
La transferencia de control al programador es lo suficientemente rápida como para que no haya un ciclo de espera activo real, ya que la CPU estará ocupada ejecutando otro subproceso y no estará inactiva. Trabajar en el espacio del usuario le permite evitar llamadas al sistema que son costosas en términos de tiempo de procesador [30] .
La arquitectura ARMv7 utiliza los llamados monitores exclusivos locales y globales para sincronizar la memoria entre los procesadores, que son máquinas de estado que controlan el acceso atómico a las celdas de memoria [31] [32] . Se puede realizar una lectura atómica de una celda de memoria mediante la instrucción LDREX[33] , y se puede realizar una escritura atómica mediante la instrucción STREX, que también devuelve el indicador de éxito de la operación [34] .
El algoritmo de captura de exclusión mutua implica leer su valor con LDREXy verificar el valor de lectura para un estado bloqueado, que corresponde al valor 1 de la variable de exclusión mutua. Si el mutex está bloqueado, se llama al código de espera de liberación de bloqueo. Si el mutex estaba en estado desbloqueado, se podría intentar el bloqueo usando la instrucción de escritura exclusiva STREXNE. Si la escritura falla porque el valor de la exclusión mutua ha cambiado, el algoritmo de captura se repite desde el principio [35] . Luego de capturar el mutex, se ejecuta la instrucción DMB, lo que garantiza la integridad de la memoria del recurso protegido por el mutex [36] .
Antes de que se libere el mutex, también se llama a la instrucción DMB, después de lo cual se escribe el valor 0 en la variable mutex usando la instrucción STR, lo que significa transferencia al estado desbloqueado. Después de desbloquear el mutex, las tareas en espera, si las hay, deben indicar que el mutex se ha liberado [35] .
Comunicación entre procesos | |
---|---|
Métodos | |
Protocolos y estándares seleccionados |