Bloqueo recursivo (Mutex) frente a locking no recursivo (Mutex)

POSIX permite que los mutexes sean recursivos. Eso significa que el mismo hilo puede bloquear el mismo mutex dos veces y no se estancará. Por supuesto, también necesita desbloquearlo dos veces, de lo contrario, ningún otro hilo puede obtener el mutex. No todos los sistemas que soportan pthreads también soportan mutexes recursivos, pero si quieren ser conformes con POSIX, tienen que hacerlo .

Otras API (más API de alto nivel) también suelen ofrecer mutexes, a menudo llamados lockings. Algunos sistemas / idiomas (por ejemplo, Cocoa Objective-C) ofrecen tanto mutexes recursivos como no recursivos. Algunos idiomas también solo ofrecen uno o el otro. Por ejemplo, en Java los mutex son siempre recursivos (el mismo hilo puede “sincronizarse” dos veces en el mismo objeto). Dependiendo de qué otra funcionalidad de subprocesos ofrezcan, no tener mutexes recursivos podría no ser un problema, ya que pueden escribirse fácilmente por usted mismo (ya implementé mutex recursivos sobre la base de operaciones más simples de mutex / condición).

Lo que realmente no entiendo: ¿para qué sirven los mutexes no recursivos? ¿Por qué querría tener un punto muerto de subproceso si bloquea el mismo mutex dos veces? Incluso los lenguajes de alto nivel que podrían evitar eso (por ejemplo, probar si esto empantana y lanzar una excepción si lo hace) generalmente no hacen eso. Dejarán que el hilo se estanque en su lugar.

¿Es esto solo para casos donde accidentalmente lo locking dos veces y lo deslocking solo una vez y en caso de un mutex recursivo, sería más difícil encontrar el problema, entonces en su lugar lo tengo un punto muerto para ver dónde aparece el locking incorrecto? ¿Pero no podría hacer lo mismo con tener un contador de locking devuelto al desbloquear y en una situación en la que estoy seguro de haber liberado el último locking y el contador no es cero, puedo lanzar una excepción o registrar el problema? ¿O hay algún otro caso de uso más útil de mutexes no recursivos que no veo? ¿O tal vez solo sea el rendimiento, ya que un mutex no recursivo puede ser ligeramente más rápido que uno recursivo? Sin embargo, probé esto y la diferencia realmente no es tan grande.

La diferencia entre un mutex recursivo y no recursivo tiene que ver con la propiedad. En el caso de un mutex recursivo, el núcleo tiene que hacer un seguimiento del hilo que realmente obtuvo el mutex la primera vez para que pueda detectar la diferencia entre la recursión frente a un hilo diferente que debería bloquear en su lugar. Como se señaló en otra respuesta, hay una cuestión de la sobrecarga adicional de esto tanto en términos de memoria para almacenar este contexto como también los ciclos necesarios para mantenerlo.

Sin embargo , hay otras consideraciones en juego aquí también.

Debido a que el mutex recursivo tiene un sentido de propiedad, el hilo que atrapa el mutex debe ser el mismo hilo que libera el mutex. En el caso de mutexes no recursivas, no hay sentido de propiedad y cualquier hilo generalmente puede liberar el mutex sin importar qué thread originalmente tomó el mutex. En muchos casos, este tipo de “mutex” es realmente más una acción de semáforo, donde no necesariamente se usa el mutex como dispositivo de exclusión, sino que se usa como dispositivo de sincronización o señalización entre dos o más hilos.

Otra propiedad que viene con un sentido de propiedad en un mutex es la capacidad de admitir herencia prioritaria. Como el kernel puede rastrear el hilo que posee el mutex y también la identidad de todos los bloqueadores, en un sistema de roscado de prioridad se hace posible escalar la prioridad del hilo que actualmente posee el mutex a la prioridad del hilo de mayor prioridad que actualmente está bloqueando en el mutex. Esta herencia evita el problema de inversión de prioridad que puede ocurrir en tales casos. (Tenga en cuenta que no todos los sistemas soportan herencia de prioridad en tales mutexes, pero es otra característica que se hace posible a través de la noción de propiedad).

Si se refiere al clásico kernel VxWorks RTOS, definen tres mecanismos:

  • mutex : admite la recursión y, opcionalmente, la herencia de prioridad
  • semáforo binario – sin recursión, sin herencia, exclusión simple, tomador y dador no tiene que ser el mismo hilo, lanzamiento de difusión disponible
  • contar semáforo : sin recurrencia ni herencia, actúa como un contador de recursos coherente desde cualquier recuento inicial deseado, el bloque de subprocesos solo donde el recuento neto contra el recurso es cero.

Nuevamente, esto varía un tanto por plataforma, especialmente lo que ellos llaman estas cosas, pero esto debería ser representativo de los conceptos y varios mecanismos en juego.

La respuesta no es eficiencia. Los mutexes no reentrantes conducen a un mejor código.

Ejemplo: A :: foo () adquiere el locking. Luego llama a B :: bar (). Esto funcionó bien cuando lo escribiste. Pero algún tiempo después alguien cambia B :: bar () para llamar a A :: baz (), que también adquiere el locking.

Bueno, si no tienes mutexes recursivos, esto se bloquea. Si los tiene, se ejecuta, pero puede romperse. A :: foo () puede haber dejado el objeto en un estado incoherente antes de llamar a bar (), suponiendo que baz () no se pudo ejecutar porque también adquiere el mutex. ¡Pero probablemente no debería correr! La persona que escribió A :: foo () supuso que nadie podía llamar a A :: baz () al mismo tiempo; esa es la razón por la cual ambos métodos adquirieron el locking.

El modelo mental correcto para usar mutexes: el mutex protege un invariante. Cuando se retiene el mutex, el invariante puede cambiar, pero antes de liberar el mutex, el invariante se restablece. Las cerraduras reentrantes son peligrosas porque la segunda vez que adquieres la cerradura no puedes estar seguro de que la invariante ya sea cierta.

Si está satisfecho con los lockings reentrantes, es solo porque no ha tenido que depurar un problema como este antes. Java por el momento tiene lockings no reentrantes en java.util.concurrent.locks, por cierto.

Según lo escrito por el propio Dave Butenhof :

“El más grande de todos los grandes problemas con los mutex recursivos es que te alientan a perder completamente la pista de tu esquema y scope de locking. Esto es mortal. Mal. Es el” devorador de hilos “. Mantienes las cerraduras por el menor tiempo posible. Periodo. Siempre. Si está llamando a algo con un candado sostenido simplemente porque no sabe que está retenido, o porque no sabe si el destinatario necesita el mutex, entonces lo tiene demasiado tiempo. apuntar una escopeta a su aplicación y apretar el gatillo. Presumiblemente comenzó a usar hilos para obtener simultaneidad, pero acaba de EVITAR la concurrencia “.

El modelo mental correcto para usar mutexes: el mutex protege un invariante.

¿Por qué estás seguro de que este es el modelo mental correcto para usar mutexes? Creo que el modelo correcto protege los datos, pero no invariantes.

El problema de proteger invariantes se presenta incluso en aplicaciones de subprocesos únicos y no tiene nada en común con los subprocesos múltiples y los exclusiones mutuas.

Además, si necesita proteger invariantes, aún puede usar semáforos binarios que nunca son recursivos.

Una razón principal por la que los mutex recursivos son útiles es en el caso de acceder a los métodos varias veces por el mismo hilo. Por ejemplo, supongamos que si el locking mutex protege un A / C bancario para que se retire, entonces si hay un arancel también asociado con ese retiro, entonces se debe usar el mismo mutex.

El único caso de buen uso para el mutex de recursión es cuando un objeto contiene múltiples métodos. Cuando cualquiera de los métodos modifica el contenido del objeto, y por lo tanto debe bloquear el objeto antes de que el estado sea consistente nuevamente.

Si los métodos utilizan otros métodos (es decir: addNewArray () llama addNewPoint () y finaliza con recheckBounds ()), pero cualquiera de esas funciones por sí mismas necesita bloquear el mutex, entonces el mutex recursivo es una situación en la que todos ganan.

Para cualquier otro caso (resolver solo códigos incorrectos, usarlo incluso en diferentes objetos) es claramente incorrecto.