c# - sobre - tags para responder
¿Por qué no se libera Mutex cuando se desecha? (9)
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
}
}
He establecido un punto de interrupción dentro del bloque if
, y ejecuté el mismo código en otra instancia de Visual Studio. Como se esperaba, la llamada .WaitOne
bloquea. Sin embargo, para mi sorpresa, tan pronto como continúo en la primera instancia y el bloque de using
termina, 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 espera.
Mi pregunta: por lo general, el objetivo de un IDisposable
es IDisposable
: limpia todo el estado en el que pones las cosas. Podría ver que quizás haya múltiples esperas y liberaciones dentro de un bloque de using
, pero cuando se descarta el controlador de Mutex, ¿no debería liberarse ¿automáticamente? En otras palabras, ¿por qué necesito llamar a ReleaseMutex
si estoy using
un bloque?
Ahora también me preocupa que si el código dentro del bloque if
falla, habré abandonado las mutexes que hay por ahí.
¿Hay algún beneficio para poner Mutex
en un bloque de using
? O, si simplemente hago una nueva instancia de Mutex
, la envuelvo en un try / catch y llamo ReleaseMutex()
dentro del bloque finally (Básicamente, implementar exactamente lo que pensé que Dispose()
haría)
Al leer la documentación de ReleaseMutex
, parece que la decisión de diseño fue que un Mutex debería liberarse conscientemente. Si no se llama ReleaseMutex
, significa una salida anormal de la sección protegida. Poner el lanzamiento en una forma definitiva o dispuesta, elude este mecanismo. Todavía eres libre de ignorar la excepción AbandonedMutexException, por supuesto.
Esta decisión de diseño se tomó hace mucho, mucho tiempo. Hace más de 21 años, mucho antes de que se imaginara .NET o se considerara la semántica de IDisposable. La clase .NET Mutex es una clase de envoltorio para el soporte del sistema operativo subyacente para mutexes. El constructor pinvokes CreateMutex , el método WaitOne WaitForSingleObject() pinvokes 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 implementaron la regla de que los subprocesos que poseen el mutex deben llamar a ReleaseMutex () antes de que salga. Y si no es así, esto es una indicación muy clara de que el subproceso terminó de manera inesperada, generalmente a través de una excepción. Lo que implica que se pierde la sincronización, un error de subprocesamiento 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. No en lo más mínimo porque no hay otra forma de probar el estado del mutex que no sea haciendo una espera. Debes llamar a ReleaseMutex (). Y tenga en cuenta que su segundo fragmento tampoco es correcto; no puedes llamarlo en un mutex que no hayas adquirido. Se debe mover dentro del cuerpo de la sentencia if ().
Necesitamos entender más que .net para saber lo que está sucediendo en el inicio de la página de MSDN; nos da la primera pista de que alguien "extraño" está ocurriendo:
Una primitiva de sincronización que también se puede utilizar 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 Muxtex vive en el espacio de direcciones de Kernal de Windows, no en el espacio de direcciones de su aplicación.
En la mayoría de los casos, es mejor utilizar un Monitor , si solo está tratando de sincronizar el acceso a los objetos dentro de un solo proceso.
Ok, publicar una respuesta a mi propia pregunta. Por lo que puedo decir, esta es la forma ideal de implementar un Mutex
que:
- Siempre se desecha
- Se
WaitOne
siWaitOne
tuvo éxito. - No se abandonará si algún código lanza una excepción.
¡Ojalá esto ayude a alguien!
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 liberar el Mutex y, en su lugar, dejarlo abandonado. En otras palabras, simplemente llame a mut.ReleaseMutex();
cuando el código termina con éxito, y no lo coloque dentro del bloque finally
. El código que adquiere el Mutex podría capturar 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 programa al mismo tiempo. Por esta razón, creo que mi solución anterior está bien, pero la tuya puede ser diferente.
Si necesita garantizar que el mutex se libera, cambie a un bloque de try catch catch y coloque el release de mutex en el bloque finally. Se supone que usted posee y tiene un identificador para el mutex. Esa lógica debe ser incluida antes de invocar el lanzamiento.
Tenga en cuenta: el objeto Mutex.Dispose () ejecutado por el recolector de basura falla porque el proceso de recolección de basura no posee el identificador de acuerdo con Windows.
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 al objeto en ese estado. Un patrón normal para el código que necesita modificar un objeto es:
- Adquirir mutex
- Realizar cambios en el objeto que hagan que su estado se vuelva inválido.
- Realizar cambios en el objeto que hagan que su estado vuelva a ser válido.
- Soltar mutex
Si algo sale mal después de que # 2 haya comenzado y antes de que # 3 haya terminado, el objeto puede quedar en un estado que no satisfaga a sus invariantes. Dado que el patrón adecuado 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 que no sea 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, después de haber sido eliminado, nunca lo será) . Por lo tanto, el curso de acción adecuado es lanzar una excepción.
Un patrón que es algo más agradable que el implementado por el objeto de mutex de .NET es que el método de "adquisición" devuelva un objeto IDisposable
que encapsula no el mutex, sino una adquisición particular de los mismos. La disposición de ese objeto liberará el mutex. El código puede verse algo así como:
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 debería invalidar el mutex llamando a Dispose
, ya que el recurso protegido puede estar en un estado dañado. Si el código interno falla en otra parte, el mutex debe liberarse ya que el recurso protegido está en un estado válido, y el código dentro del bloque que using
no necesitará acceder a él más. No tengo recomendaciones particulares de bibliotecas que implementen ese patrón, pero no es particularmente difícil de implementar como una envoltura alrededor de otros tipos de exclusión mutua.
La documentación explica (en la sección "Comentarios") que existe una diferencia conceptual entre crear una instancia de un objeto Mutex (que, de hecho, no hace nada especial en lo que respecta a la sincronización) y adquirir un Mutex (mediante WaitOne
). Tenga en cuenta que:
-
WaitOne
devuelve un valor booleano, lo que significa que la adquisición de un Mutex puede fallar (tiempo de espera) y ambos casos deben manejarse - Cuando
WaitOne
devuelvetrue
, entonces el hilo que llama ha adquirido el Mutex y debe llamar aReleaseMutex
, o de lo contrario el Mutex quedará abandonado - Cuando devuelve
false
, el subproceso de llamada no debe llamar aReleaseMutex
Por lo tanto, hay más para Mutexes que la instanciación. En cuanto a si debe usar el using
todos modos, echemos un vistazo a lo que Dispose
hace (como se hereda de WaitHandle
):
protected virtual void Dispose(bool explicitDisposing)
{
if (this.safeWaitHandle != null)
{
this.safeWaitHandle.Close();
}
}
Como podemos ver, el Mutex no se ha lanzado, 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, por supuesto, puede usar un bloque try/finally
para asegurarse de que, si se adquiere el Mutex, se libere correctamente. Este es probablemente el enfoque más directo.
Si realmente no le importa el caso en el que el Mutex no se adquiere (lo que no ha indicado, ya que le pasa un TimeSpan
a WaitOne
), puede WaitOne
Mutex
en su propia clase que implementa IDisposable
, adquirir el Mutex en el constructor (utilizando WaitOne()
sin argumentos), y WaitOne()
dentro de Dispose
. Aunque, probablemente no lo recomendaría, ya que esto causaría que sus hilos esperen indefinidamente si algo sale mal, y sin importar que existan buenas razones para manejar explícitamente ambos casos al intentar una adquisición, como lo menciona @HansPassant.
Dispose
depende de WaitHandle
para ser lanzado. Por lo tanto, aunque se using
llamadas Dispose
, no entrará en vigor hasta que se cumplan las condiciones de estado estable. Cuando llama a ReleaseMutex
le está diciendo al sistema que está liberando el recurso, y por lo tanto, es libre de desecharlo.