Bloqueo Recursivo (Mutex) vs Bloqueo No Recursivo (Mutex)

183

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 admiten pthreads también admiten mutex recursivos, pero si quieren cumplir con POSIX, deben hacerlo .

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

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

¿Es esto solo para casos en los que accidentalmente lo bloqueo dos veces y solo lo desbloqueo una vez y en caso de un mutex recursivo, sería más difícil encontrar el problema, por lo que en su lugar tengo un punto muerto inmediato para ver dónde aparece el bloqueo incorrecto? Pero, ¿no podría hacer lo mismo con un contador de bloqueo devuelto al desbloquear y en una situación en la que estoy seguro de que liberé el último bloqueo 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 sea solo rendimiento, ya que un mutex no recursivo puede ser un poco más rápido que uno recursivo? Sin embargo, probé esto y la diferencia realmente no es tan grande.

Mecki
fuente

Respuestas:

154

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 bloquearse. Como señaló 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 toma el mutex debe ser el mismo hilo que libera el mutex. En el caso de mutexes no recursivos, no hay sentido de propiedad y cualquier hilo puede liberar el mutex sin importar qué hilo originalmente tomó el mutex. En muchos casos, este tipo de "mutex" es realmente más una acción de semáforo, donde no necesariamente está usando el mutex como dispositivo de exclusión, sino que lo 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 la herencia prioritaria. Debido a que el kernel puede rastrear el subproceso que posee el mutex y también la identidad de todos los bloqueadores, en un sistema de subprocesos de prioridad es posible escalar la prioridad del subproceso que actualmente posee el mutex a la prioridad del subproceso 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 admiten herencia prioritaria 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 kernel clásico de VxWorks RTOS, definen tres mecanismos:

  • mutex : admite recursividad y, opcionalmente, herencia prioritaria. Este mecanismo se usa comúnmente para proteger secciones críticas de datos de manera coherente.
  • semáforo binario : sin recursión, sin herencia, simple exclusión, tomador y donante no tiene que ser el mismo hilo, lanzamiento de difusión disponible. Este mecanismo se puede utilizar para proteger secciones críticas, pero también es particularmente útil para la señalización coherente o la sincronización entre subprocesos.
  • contando el semáforo : sin recursividad ni herencia, actúa como un contador de recursos coherente de cualquier recuento inicial deseado, los subprocesos solo bloquean donde el recuento neto contra el recurso es cero.

Nuevamente, esto varía un poco según la plataforma, especialmente lo que llaman estas cosas, pero esto debería ser representativo de los conceptos y diversos mecanismos en juego.

Jeff alto
fuente
9
Su explicación sobre el mutex no recursivo sonaba más como un semáforo. Un mutex (ya sea recursivo o no recursivo) tiene una noción de propiedad.
Jay D
@ JayD Es muy confuso cuando las personas discuten sobre cosas como estas ... entonces, ¿quién es la entidad que define estas cosas?
Pacerier
13
@Pacerier El estándar relevante. Esta respuesta es, por ejemplo, incorrecta para posix (pthreads), donde desbloquear un mutex normal en un hilo que no sea el hilo que lo bloqueó es un comportamiento indefinido, mientras que hacer lo mismo con una comprobación de errores o un mutex recursivo da como resultado un código de error predecible. Otros sistemas y estándares pueden comportarse de manera muy diferente.
nos
Quizás esto sea ingenuo, pero tenía la impresión de que la idea central de un mutex es que el hilo de bloqueo desbloquea el mutex y luego otros hilos pueden hacer lo mismo. De computing.llnl.gov/tutorials/pthreads :
user657862
2
@curiousguy: una versión de difusión libera todos y cada uno de los hilos bloqueados en el semáforo sin darlo explícitamente (permanece vacío), mientras que una entrega binaria normal solo liberaría el hilo en la cabecera de la cola de espera (suponiendo que haya uno bloqueado).
Tall Jeff
123

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

Ejemplo: A :: foo () adquiere el bloqueo. 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 bloqueo.

Bueno, si no tienes mutex recursivos, este punto muerto. Si los tiene, se ejecuta, pero puede romperse. A :: foo () puede haber dejado el objeto en un estado inconsistente antes de llamar a bar (), bajo el supuesto de que baz () no pudo ejecutarse porque también adquiere el mutex. ¡Pero probablemente no debería funcionar! La persona que escribió A :: foo () asumió que nadie podía llamar a A :: baz () al mismo tiempo, esa es la razón por la cual ambos métodos adquirieron el bloqueo.

El modelo mental adecuado para usar mutexes: el mutex protege a un invariante. Cuando se mantiene 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 ya no puedes estar seguro de que la invariante sea verdadera.

Si está satisfecho con las cerraduras reentrantes, es solo porque no ha tenido que depurar un problema como este antes. Java tiene bloqueos no reentrantes en estos días en java.util.concurrent.locks, por cierto.

Jonathan
fuente
44
Me tomó un tiempo entender lo que estabas diciendo acerca de que el invariante no era válido cuando agarras la cerradura por segunda vez. ¡Buen punto! ¿Qué pasaría si fuera un bloqueo de lectura y escritura (como ReadWriteLock de Java) y usted adquiriera el bloqueo de lectura y luego volviera a adquirir el bloqueo de lectura por segunda vez en el mismo hilo? No invalidarías una invariante después de adquirir un bloqueo de lectura, ¿verdad? Entonces, cuando adquiere el segundo bloqueo de lectura, la invariante sigue siendo cierta.
dgrant
1
@ Jonathan ¿ Java tiene bloqueos no reentrantes en estos días en java.util.concurrent.locks ?
user454322
1
+1 Supongo que el uso más común para el bloqueo reentrante es dentro de una sola clase, donde se pueden invocar algunos métodos desde piezas de código protegidas y no protegidas. En realidad, esto siempre se puede factorizar. @ user454322 Claro, Semaphore.
maaartinus
1
Disculpe mi malentendido, pero no veo cómo esto es relevante para mutex. Supongamos que no hay múltiples subprocesos y bloqueos involucrados, A::foo()aún puede haber dejado el objeto en un estado inconsistente antes de llamar A::bar(). ¿Qué tiene que ver mutex, recursivo o no, con este caso?
Siyuan Ren
1
@SiyuanRen: El problema es poder razonar localmente sobre el código. Las personas (al menos yo) están entrenadas para reconocer las regiones bloqueadas como mantenimiento invariante, es decir, en el momento en que adquieres el bloqueo, ningún otro hilo está modificando el estado, por lo que los invariantes en la región crítica se mantienen. Esta no es una regla difícil, y puede codificar sin tener en cuenta a los invariantes, pero eso haría que su código sea más difícil de razonar y mantener. Lo mismo sucede en modo de un solo subproceso sin mutexes, pero allí no estamos capacitados para razonar localmente alrededor de la región protegida.
David Rodríguez - dribeas
92

Según lo escrito por el propio Dave Butenhof :

"El mayor de todos los grandes problemas con los mutex recursivos es que te alientan a perder completamente la noción de tu esquema y alcance de bloqueo. Esto es mortal. Malvado. Es el" devorador de hilos ". Mantienes bloqueos por el tiempo más corto posible. Período. Siempre. Si está llamando a algo con un candado retenido simplemente porque no sabe que está retenido, o porque no sabe si la persona que llama necesita el mutex, entonces lo está reteniendo demasiado tiempo. apuntando una escopeta a su aplicación y apretando el gatillo. Presumiblemente comenzó a usar hilos para obtener concurrencia; pero acaba de PREVENIR la concurrencia ".

Chris Cleeland
fuente
9
También tenga en cuenta la parte final en la respuesta de Butenhof: ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322
2
También dice que usar un solo mutex recursivo global (su opinión es que solo necesita uno) está bien como una muleta para posponer conscientemente el arduo trabajo de comprender las invarianzas de una biblioteca externa cuando comienza a usarlo en código multiproceso. Pero no debe usar muletas para siempre, sino que finalmente invierta el tiempo para comprender y corregir las invariantes de concurrencia del código. Entonces podríamos parafrasear que usar mutex recursivo es una deuda técnica.
FooF
13

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

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

El problema de proteger invariantes se presenta incluso en aplicaciones de un solo subproceso y no tiene nada en común con subprocesos múltiples y mutexes.

Además, si necesita proteger invariantes, aún puede usar un semáforo binario que nunca es recursivo.


fuente
Cierto. Hay mejores mecanismos para proteger a un invariante.
ActiveTrayPrntrTagDataStrDrvr
8
Esto debería ser un comentario a la respuesta que ofreció esa declaración. Los mutexes no solo protegen los datos, sino que también protegen a los invariantes. Intente escribir un contenedor simple (el más simple es una pila) en términos de átomos (donde los datos se protegen a sí mismos) en lugar de mutexes y comprenderá la declaración.
David Rodríguez - dribeas
Los mutexes no protegen los datos, protegen a los invariantes. Sin embargo, ese invariante puede usarse para proteger los datos.
Jon Hanna
4

Una razón principal por la que los mutexes recursivos son útiles es en caso de acceder a los métodos varias veces por el mismo hilo. Por ejemplo, supongamos que si el bloqueo de mutex está protegiendo un banco A / c para retirar, entonces si hay una tarifa también asociada con ese retiro, entonces se debe usar el mismo mutex.

avis
fuente
4

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

Si los métodos usan otros métodos (es decir: addNewArray () llama a addNewPoint () y finaliza con recheckBounds ()), pero cualquiera de esas funciones necesita bloquear el mutex, entonces el mutex recursivo es un win-win.

¡Para cualquier otro caso (resolver solo una mala codificación, usarlo incluso en diferentes objetos) es claramente incorrecto!

Ceros oscuros
fuente
1

¿Para qué sirven los mutexes no recursivos?

Son absolutamente buenos cuando tienes que asegurarte de que el mutex esté desbloqueado antes de hacer algo. Esto se debe a que pthread_mutex_unlockpuede garantizar que el mutex esté desbloqueado solo si no es recursivo.

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

Si g_mutexno es recursivo, se garantiza que el código anterior llamará bar()con el mutex desbloqueado .

Por lo tanto, eliminar la posibilidad de un punto muerto en el caso de bar()que sea una función externa desconocida que bien puede hacer algo que puede provocar que otro hilo intente adquirir el mismo mutex. Tales escenarios no son infrecuentes en aplicaciones creadas en grupos de subprocesos y en aplicaciones distribuidas, donde una llamada entre procesos puede generar un nuevo subproceso sin que el programador del cliente se dé cuenta de eso. En todos estos escenarios, es mejor invocar dichas funciones externas solo después de liberar el bloqueo.

Si g_mutexfuera recursivo, simplemente no habría forma de asegurarse de que esté desbloqueado antes de realizar una llamada.

Igor G
fuente