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.
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.
fuente
Semaphore
.A::foo()
aún puede haber dejado el objeto en un estado inconsistente antes de llamarA::bar()
. ¿Qué tiene que ver mutex, recursivo o no, con este caso?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 ".
fuente
...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
¿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
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.
fuente
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!
fuente
Son absolutamente buenos cuando tienes que asegurarte de que el mutex esté desbloqueado antes de hacer algo. Esto se debe a que
pthread_mutex_unlock
puede garantizar que el mutex esté desbloqueado solo si no es recursivo.Si
g_mutex
no 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_mutex
fuera recursivo, simplemente no habría forma de asegurarse de que esté desbloqueado antes de realizar una llamada.fuente