Pregunta

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 ser POSIX conforme, tienen que .

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. P.ej. en Java, los mutexes son siempre 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é mis mutex recursivos 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. En su lugar, dejarán que el hilo se bloquee.

¿Es esto solo para los casos, donde 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, así que en cambio tengo un punto muerto inmediato para ver dónde está el bloqueo incorrecto aparece? ¿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 es 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.

¿Fue útil?

Solución

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 recursividad 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 soportar herencia prioritaria. Debido a que el núcleo 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 la recursión y, opcionalmente, la 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.
  • recuento de semáforos : sin recursión ni herencia, actúa como un contador de recursos coherente de cualquier recuento inicial deseado, los hilos 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 los diversos mecanismos en juego.

Otros consejos

La respuesta es no 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 () 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 bloqueo.

El modelo mental correcto 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 no puedes estar seguro de que la invariante sea verdadera más.

Si está satisfecho con los bloqueos 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.

Según lo escrito por el propio Dave Butenhof :

" El mayor de todos los grandes problemas con mutexes recursivos es que te alientan a perder completamente el rastro de tu esquema de bloqueo y alcance. Esto es mortal Mal. Es el "comehilos". Tienes cerraduras para El tiempo más corto posible. Período. Siempre. Si estas llamando algo con un candado retenido simplemente porque no sabes que está retenido, o porque no sabes si la persona que llama necesita el mutex, entonces estás sosteniéndolo demasiado tiempo Estás apuntando una escopeta a tu aplicación y apretando el gatillo. Probablemente comenzó a usar hilos para obtener concurrencia pero acaba de PREVENIR la concurrencia. "

  

El modelo mental correcto para usar   mutexes: el mutex protege 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.

Una razón principal por la cual 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 protege un A / c bancario para retirar, entonces si hay una tarifa también asociada con ese retiro, entonces se debe usar el mismo mutex.

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 beneficio mutuo.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top