2008-10-09 31 views
152

POSIX permite mutex para ser recursivo. 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 son compatibles con mutexes recursivos, pero si quieren ser POSIX conform, they have to.recursiva de bloqueo (mutex) vs Bloqueo no recursiva (mutex)

Otras API (API de más alto nivel) también suelen ofrecer exclusiones mutuas, a menudo llamados bloqueos. 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. P.ej. en Java los mutexes son siempre recursivos (el mismo hilo puede "sincronizarse" dos veces en el mismo objeto). Dependiendo de qué funcionalidad subproceso que ofrecen, no tener mutex recursivo podrían haber ningún problema, ya que fácilmente pueden escribirse a sí mismo (que ya se han aplicado los mutex recursivas a mí mismo sobre la base de más simples operaciones mutex/condición).

Lo que realmente no entiendo: ¿Cuáles son los mutex no recursivos bueno para? ¿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 se estancará y lanzar una excepción si lo hace) generalmente no hacen eso. Dejarán que el hilo se estanque en su lugar.

Esto es solo para casos, donde accidentalmente lo bloqueo dos veces y lo desbloqueo solo una vez y en caso de un mutex recursivo, sería más difícil encontrar el problema, por lo que lo tengo punto muerto inmediatamente para ver dónde está el incorrecto bloqueo aparece? ¿Pero no podría hacer lo mismo con tener un contador de bloqueo devuelto al desbloquear y en una situación en la que estoy seguro de haber liberado 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 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.

Respuesta

123

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 agarra la exclusión mutua 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 de prioridad. 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 del núcleo VxWorks RTOS, se definen tres mecanismos:

  • mutex - apoya la recursividad y la herencia de prioridad opcionalmente
  • semáforo binario - sin recursividad, sin herencia, sencillo exclusión, tomador y dador no tiene que ser el mismo hilo, lanzamiento de difusión disponible
  • contando el semáforo - sin recurrencia o herencia, actúa como un contador de recursos coherente de cualquier recuento inicial deseado, solo los subprocesos bloquean cuando el conteo 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.

+4

su explicación sobre el mutex no recursivo sonaba más como un semáforo. Un mutex (recursivo o no recursivo) tiene una noción de propiedad. –

+0

@JayD Es muy confuso cuando la gente discute sobre cosas como estas ... ¿quién es la entidad que define estas cosas? – Pacerier

+8

@Pacerier El estándar relevante. Esta respuesta es, p. mal 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 un error de comprobación o mutex recursivo da como resultado un código de error predicable. Otros sistemas y estándares pueden comportarse de manera muy diferente. – nos

103

La respuesta es no eficiencia. Los mutexes 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 tiene mutexes recursivos, estos puntos muertos. 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 bloqueo.

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 bloqueos de reentrada, es solo porque no ha tenido que depurar un problema como este anteriormente. Java por el momento tiene bloqueos no reentrantes en java.util.concurrent.locks, por cierto.

+4

Me llevó algo de tiempo obtener lo que usted estaba diciendo acerca de que el invariante no es válido cuando agarra el candado una segunda vez. ¡Buen punto! ¿Qué pasa si se trata de un bloqueo de lectura y escritura (como ReadWriteLock de Java) y usted adquirió el bloqueo de lectura y luego volvió a adquirir el bloqueo de lectura por segunda vez en el mismo hilo. No invalidaría un invariante después de adquirir un bloqueo de lectura ¿no? Entonces cuando adquieres el segundo bloqueo de lectura, el invariante sigue siendo verdadero. – dgrant

+1

@Jonathan ¿_Java tiene bloqueos no reentrantes estos días en java.util.concurrent.locks_ ?? – user454322

+1

+1 Supongo que el uso más común para el bloqueo de reentrada es dentro de una sola clase, donde se pueden llamar algunos métodos desde piezas de código guardadas y no custodiadas. Esto puede ser siempre factorizado. @ user454322 Seguro, 'Semaphore'. – maaartinus

10

El modelo mental adecuado para el uso mutex: La exclusión mutua 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 la protección de invariantes se presenta incluso en aplicaciones de subproceso único y no tiene nada en común con multi-threading y mutexes.

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

+0

Es cierto. Hay mejores mecanismos para proteger un invariante. – ActiveTrayPrntrTagDataStrDrvr

+6

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. Intenta escribir un contenedor simple (el más simple es una pila) en términos de átomos (donde los datos se protegen) en lugar de mutex y comprenderás la afirmación. –

+0

Los mutexes no protegen los datos, sino que protegen a los invariantes. Sin embargo, ese invariante puede usarse para proteger los datos. –

78

As written by Dave Butenhof himself:

"El más grande de todos los grandes problemas con los mutex recursivo es que que le animan a perder por completo la pista de su esquema de bloqueo y alcance.Esto es mortal Mal. Es el "devorador de hilos". Usted mantiene bloqueos durante el tiempo más corto posible. Período. Siempre. Si llama al algo con un candado sostenido simplemente porque no sabe que está retenido, o porque no sabe si el destinatario necesita el mutex, entonces está aguantándolo demasiado. Apuntas una escopeta a tu aplicación y apretando el gatillo. Probablemente comenzó a usar hilos para obtener la concurrencia ; pero acaba de PREVENIR la concurrencia. "

+8

También tenga en cuenta la parte final en la respuesta del Butenhof: '... que no hemos terminado hasta que estén [mutex recursivo] todas ha ido .. O sentarse y dejar que alguien más haga el design.' – user454322

+1

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 usarla en código multiproceso. Pero no debe usar muletas para siempre, pero eventualmente invertir el tiempo para comprender y corregir los invariantes de concurrencia del código. Así que podríamos parafrasear que usar mutex recursivo es deuda técnica. – FooF

2

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, digamos si el bloqueo mutex protege un banco de A/c retirar, luego si hay una tarifa asociada también con ese retiro, entonces se debe usar el mismo mutex.

0

El único caso de buen uso para la recursión mutex 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 de nuevo.

Si los métodos utilizan otros métodos (es decir: addNewArray() llama addNewPoint(), y finaliza con rec heckBounds()), pero cualquiera de esas funciones por sí mismas necesita bloquear el mutex, entonces mutex recursivo es un ganar-ganar.

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