¿Por qué no se libera Mutex cuando se elimina?

Tengo el siguiente código:

using (Mutex mut = new Mutex(false, MUTEX_NAME)) { if (mut.WaitOne(new TimeSpan(0, 0, 30))) { // Some code that deals with a specific TCP port // Don't want this to run at the same time in another process } } 

Establecí un punto de interrupción dentro del bloque if y ejecuté el mismo código dentro de otra instancia de Visual Studio. Como se esperaba, los bloques de llamada .WaitOne . Sin embargo, para mi sorpresa, tan pronto como continúo en la primera instancia y el bloque de using finaliza, recibo una excepción en el segundo proceso sobre un Mutex abandonado.

La solución es llamar a ReleaseMutex :

 using (Mutex mut = new Mutex(false, MUTEX_NAME)) { if (mut.WaitOne(new TimeSpan(0, 0, 30))) { // Some code that deals with a specific TCP port // Don't want this to run twice in multiple processes } mut.ReleaseMutex(); } 

Ahora, las cosas funcionan como se esperaba.

Mi pregunta: Por lo general, el objective de un IDisposable es limpiar cualquier estado en el que pones cosas. Pude ver quizás tener múltiples esperas y lanzamientos dentro de un bloque de using , pero cuando se elimina el identificador del Mutex, ¿no debería liberarse? ¿automáticamente? En otras palabras, ¿por qué tengo que llamar a ReleaseMutex si estoy en un bloque de using ?

También estoy preocupado de que si el código dentro del bloque if se cuelga, habrá abandonado los mutexes.

¿Hay algún beneficio para poner Mutex en un bloque de using ? O, ¿debería actualizar una instancia de Mutex , envolverla en try / catch y llamar a ReleaseMutex() dentro del bloque finally (básicamente implementando exactamente lo que pensé que Dispose() haría)

La documentación explica (en la sección “Observaciones”) que existe una diferencia conceptual entre crear instancias de un objeto Mutex (que, de hecho, no hace nada especial en lo que respecta a la sincronización) y adquirir un Mutex (usando WaitOne ). Tenga en cuenta que:

  • WaitOne devuelve un booleano, lo que significa que la adquisición de un Mutex puede fallar (tiempo de espera) y ambos casos deben ser manejados
  • Cuando WaitOne devuelve true , el hilo que llama ha adquirido el Mutex y debe llamar a ReleaseMutex , de lo contrario el Mutex quedará abandonado.
  • Cuando devuelve false , el hilo de llamada no debe llamar a ReleaseMutex

Entonces, hay mucho más en Mutexes que instanciación. En cuanto a si deberías utilizar de todos modos, veamos qué hace Dispose ( heredado de WaitHandle ):

 protected virtual void Dispose(bool explicitDisposing) { if (this.safeWaitHandle != null) { this.safeWaitHandle.Close(); } } 

Como podemos ver, el Mutex no se lanza, pero hay algo de limpieza involucrado, por lo que seguir con el using sería un buen enfoque.

En cuanto a cómo debe proceder, puede usar un bloque try/finally para asegurarse de que, si se adquiere el Mutex, se libera correctamente. Este es probablemente el enfoque más directo.

Si realmente no le importa el caso en el que no se adquiera el Mutex (que no ha indicado, ya que pasa un TimeSpan a WaitOne ), podría ajustar Mutex en su propia clase que implementa IDisposable , adquirir el Mutex en el constructor (usando WaitOne() sin argumentos), y WaitOne() dentro de Dispose . Aunque, probablemente no recomendaría esto, ya que esto haría que sus subprocesos esperen indefinidamente si algo sale mal, y sin importar hay buenas razones para manejar explícitamente ambos casos al intentar una adquisición, como lo menciona @HansPassant.

Esta decisión de diseño se tomó hace mucho, mucho tiempo. Hace más de 21 años, mucho antes de que se pensara .NET o se consideró alguna vez la semántica de IDisposable. La clase .NET Mutex es una clase contenedora para el soporte de sistema operativo subyacente para mutexes. El constructor identifica CreateMutex , el método WaitOne () identifica WaitForSingleObject () .

Tenga en cuenta el valor de retorno WAIT_ABANDONED de WaitForSingleObject (), que es el que genera la excepción.

Los diseñadores de Windows colocaron la regla dura como un hilo que posee el mutex debe llamar a ReleaseMutex () antes de que se cierre. Y si no es así, esta es una indicación muy fuerte de que el hilo terminó de una manera inesperada, generalmente a través de una excepción. Lo que implica que se pierde la sincronización, un error de enhebrado muy serio. Compare con Thread.Abort (), una forma muy peligrosa de terminar un hilo en .NET por la misma razón.

Los diseñadores de .NET no alteraron de ninguna manera este comportamiento. De ninguna manera, porque no hay forma de probar el estado del mutex aparte de realizar una espera. Debe llamar a ReleaseMutex (). Y tenga en cuenta que su segundo fragmento tampoco es correcto; no puedes llamarlo con un mutex que no hayas adquirido. Debe moverse dentro del cuerpo de la statement if ().

Uno de los usos principales de un mutex es garantizar que el único código que verá un objeto compartido en un estado que no satisfaga sus invariantes sea el código que (con suerte) temporalmente ponga el objeto en ese estado. Un patrón normal para el código que necesita modificar un objeto es:

  1. Adquirir mutex
  2. Haga cambios en el objeto que causa que su estado se vuelva inválido
  3. Realice cambios en el objeto que hacen que su estado vuelva a ser válido
  4. Liberar mutex

Si algo sale mal después de que el # 2 haya comenzado y antes de que el # 3 haya finalizado, el objeto puede quedar en un estado que no satisfaga sus invariantes. Como el patrón correcto es liberar un mutex antes de eliminarlo, el hecho de que el código elimine un mutex sin liberarlo implica que algo salió mal en alguna parte. Como tal, puede no ser seguro que el código ingrese al mutex (ya que no se ha liberado), pero no hay razón para esperar a que se libere el mutex (ya que – habiendo sido desechado – nunca lo será) . Por lo tanto, el curso de acción apropiado es arrojar una excepción.

Un patrón que es algo mejor que el implementado por el objeto mutex .NET es hacer que el método “adquirir” devuelva un objeto IDisposable que encapsula no el mutex, sino más bien una adquisición particular del mismo. Al desechar ese objeto, se liberará el mutex. El código puede verse más o menos así:

 using(acq = myMutex.Acquire()) { ... stuff that examines but doesn't modify the guarded resource acq.EnterDanger(); ... actions which might invalidate the guarded resource ... actions which make it valid again acq.LeaveDanger(); ... possibly more stuff that examines but doesn't modify the resource } 

Si el código interno falla entre EnterDanger y LeaveDanger , entonces el objeto de adquisición debe invalidar el mutex llamando a Dispose , ya que el recurso guardado puede estar en un estado dañado. Si el código interno falla en otro lugar, se debe liberar el mutex ya que el recurso protegido está en un estado válido, y el código dentro del bloque de using ya no necesitará acceder a él. No tengo ninguna recomendación particular de bibliotecas que implementen ese patrón, pero no es particularmente difícil de implementar como un contenedor de otros tipos de mutex.

Ok, publicando una respuesta a mi propia pregunta. Por lo que puedo decir, esta es la forma ideal de implementar un Mutex que:

  1. Siempre se disipa
  2. Se WaitOne si WaitOne fue exitoso.
  3. No será abandonado si algún código arroja una excepción.

¡Espero que esto ayude a alguien a salir!

 using (Mutex mut = new Mutex(false, MUTEX_NAME)) { if (mut.WaitOne(new TimeSpan(0, 0, 30))) { try { // Some code that deals with a specific TCP port // Don't want this to run twice in multiple processes } catch(Exception) { // Handle exceptions and clean up state } finally { mut.ReleaseMutex(); } } } 

Actualización: algunos pueden argumentar que si el código dentro del bloque try pone su recurso en un estado inestable, no debe lanzar el Mutex y en su lugar dejarlo abandonado. En otras palabras, solo llame a mut.ReleaseMutex(); cuando el código termina exitosamente, y no lo coloca dentro del bloque finally . El código que adquiere el Mutex podría detectar esta excepción y hacer lo correcto .

En mi situación, realmente no estoy cambiando ningún estado. Estoy usando temporalmente un puerto TCP y no puedo ejecutar otra instancia del progtwig al mismo tiempo. Por esta razón, creo que mi solución anterior está bien, pero la suya puede ser diferente.

Necesitamos entender más que .net para saber qué está pasando al comienzo de la página de MSDN, lo que da la primera pista de que alguien “extraño” está sucediendo:

Una primitiva de sincronización que también se puede usar para la sincronización entre procesos .

Un Mutex es un ” Objeto con nombre ” de Win32 , cada proceso lo bloquea por su nombre , el objeto .NET es solo un envoltorio alrededor de las llamadas de Win32. El propio Muxtex vive dentro del espacio de direcciones Kernal de Windows, no en el espacio de direcciones de la aplicación.

En la mayoría de los casos, es mejor usar un monitor , si solo está tratando de sincronizar el acceso a los objetos en un solo proceso.

Si necesita garantizar que se libere el mutex, cambie a un bloque try catch finally y coloque el release mutex en el bloque finally. Se supone que posee y tiene un control para el mutex. Esa lógica debe incluirse antes de invocar la versión.

Al leer la documentación de ReleaseMutex , parece que la decisión de diseño fue que un Mutex debería ser lanzado conscientemente. si no se llama a ReleaseMutex , significa una salida anormal de la sección protegida. poner la liberación en un final o disponer, evita este mecanismo. aún eres libre de ignorar AbandonedMutexException, por supuesto.

Tenga en cuenta: El Mutex.Dispose () ejecutado por el recolector de basura falla porque el proceso de recolección de basura no posee el controlador según Windows.

Dispose depende de WaitHandle para ser lanzado. Por lo tanto, aunque using llamadas Dispose , no entrará en efecto hasta que se cumplan las condiciones de estado estable. Cuando llamas a ReleaseMutex le ReleaseMutex al sistema que estás liberando el recurso y, por lo tanto, puedes deshacernos de él.