为什么互斥锁在被释放时不释放?

本文关键字:释放 为什么 | 更新日期: 2023-09-27 18:19:04

我有以下代码:

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
    }
}

我在if块中设置了一个断点,并在Visual Studio的另一个实例中运行相同的代码。正如预期的那样,.WaitOne调用阻塞。然而,令我惊讶的是,当我在第一个实例中继续并且using块终止时,我在第二个进程中得到一个关于废弃互斥锁的异常。

修复是调用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();
}

现在,一切正常。

我的问题:通常IDisposable的重点是清理无论你把东西放在什么状态。我可以看到也许在using块中有多个等待释放,但是当互斥锁的句柄被处置时,它不应该自动释放吗?换句话说,如果我在using块中,为什么我需要调用ReleaseMutex ?

我现在还担心,如果if块内的代码崩溃,我将放弃互斥锁。

Mutex放在using块中有什么好处吗?或者,我应该只是新建一个Mutex实例,将其封装在try/catch中,并在finally块中调用ReleaseMutex()(基本上完全实现了我认为 Dispose()应该做的事情)

为什么互斥锁在被释放时不释放?

文档解释(在"备注"部分)在实例化一个互斥对象(实际上不是)之间存在概念上的区别,做任何特殊的事情,只要同步进行)和获取一个互斥锁(使用WaitOne)。注意:

  • WaitOne返回一个布尔值,这意味着获取互斥锁可能失败(超时),这两种情况都必须处理
  • WaitOne返回true时,则调用线程已经获得了互斥锁,必须调用ReleaseMutex,否则互斥锁将被放弃
  • 当它返回false时,那么调用线程不能调用ReleaseMutex

所以,互斥锁不仅仅是实例化。至于是否应该使用using,让我们来看看Dispose做了什么(继承自WaitHandle):

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

我们可以看到,互锁是没有释放的,但有一些清理涉及,所以坚持使用using将是一个很好的方法。

至于你应该怎么做,你当然可以使用try/finally块来确保,如果互斥锁被获取,它会被正确释放。这可能是最直接的方法。

如果您真的不关心互斥锁无法获得的情况(您没有指出,因为您将TimeSpan传递给WaitOne),您可以将Mutex包装在实现IDisposable的自己的类中,在构造函数中获取互斥锁(使用不带参数的WaitOne()),并在Dispose中释放它。尽管如此,我可能不建议这样做,因为如果出现问题,这会导致线程无限期地等待,而且在尝试获取时,有很好的理由显式处理这两种情况,正如@HansPassant所提到的。

这个设计决定是在很久很久以前做出的。21年前,早在。net被设想或IDisposable的语义被考虑之前。. net互斥锁类是一个包装器类,用于底层操作系统对互斥锁的支持。构造函数调用CreateMutex, WaitOne()方法调用WaitForSingleObject()。

注意WaitForSingleObject()的WAIT_ABANDONED返回值,它是产生异常的那个。

Windows设计人员制定了一个严格的规则,即拥有互斥锁的线程必须在退出之前调用ReleaseMutex()。如果没有,这是一个非常强烈的迹象,表明线程以意想不到的方式终止,通常是通过异常。这意味着丢失了同步,这是一个非常严重的线程错误。与thread . abort()相比,thread . abort()是一种非常危险的方式,在. net中出于同样的原因终止线程。

. net设计者没有以任何方式改变这种行为。一点也不重要,因为除了执行等待之外,没有任何方法可以测试互斥锁的状态。必须调用ReleaseMutex()。注意,你的第二个片段也不正确;你不能在没有获取的互斥对象上调用它。它必须移动到if()语句体内部。

好的,贴出我自己问题的答案。据我所知,这个是实现Mutex的理想方式:

  1. 总是被处理掉
  2. 如果WaitOne成功,则释放
  3. 如果任何代码抛出异常,
  4. 不会被丢弃。

希望这能帮助到一些人!

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();
        }
    }
}

Update:有些人可能会争辩说,如果try块中的代码使您的资源处于不稳定状态,您应该释放互斥锁,而是让它被丢弃。换句话说,当代码成功完成时,只调用mut.ReleaseMutex();,而不将其放在finally块中。获取互斥锁的代码可以捕获这个异常,然后做正确的事情

在我的情况下,我没有改变任何状态。我暂时使用TCP端口,不能同时运行该程序的另一个实例。出于这个原因,我认为我上面的解决方案是好的,但你的可能不同。

互斥锁的主要用途之一是确保唯一能够看到处于不满足其不变量状态的共享对象的代码是将该对象置于该状态的代码(希望是暂时的)。需要修改对象的代码的正常模式是:

  1. 获得互斥
  2. 修改对象,使其状态变为无效
  3. 对对象进行更改,使其状态再次有效
  4. 释放互斥

如果在#2开始之后和#3结束之前出现问题,对象可能处于不满足其不变量的状态。由于正确的模式是在释放互斥锁之前释放它,因此代码在不释放互斥锁的情况下释放互斥锁这一事实意味着某个地方出了问题。因此,代码进入互斥锁可能不安全(因为它还没有被释放),但是没有理由等待互斥锁被释放(因为——已经被释放——它永远不会被释放)。因此,正确的做法是抛出异常。

一个比。net互斥对象实现的模式更好的模式是让"acquire"方法返回一个IDisposable对象,该对象不是封装互斥对象,而是封装其特定的获取。处理该对象将释放互斥锁。代码看起来像这样:

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
}

如果内部代码在EnterDangerLeaveDanger之间失败,那么获取对象应该通过调用Dispose来使互斥锁无效,因为被保护的资源可能处于损坏状态。如果内部代码在其他地方失败,则应该释放互斥锁,因为受保护的资源处于有效状态,并且using块中的代码不再需要访问它。我对实现这种模式的库没有任何特别的建议,但是作为其他类型互斥锁的包装来实现它并不是特别困难。

我们需要比。net了解更多,才能知道MSDN页面的开始是什么,这是第一个暗示,有人"奇怪"正在发生:

同步原语,也可用于进程间同步。

互斥锁是一个Win32 " 命名对象 ",每个进程通过名称锁定它, .net对象只是Win32调用的包装。Muxtex本身位于Windows内核地址空间中,而不是您的应用程序地址空间。

在大多数情况下,如果您只是试图在单个进程中同步对对象的访问,那么您最好使用Monitor。

如果需要保证互斥锁被释放,则切换到try catch finally块并将互斥锁释放放在finally块中。假定您拥有并拥有互斥锁的句柄。需要在调用发布之前包含该逻辑。

阅读ReleaseMutex的文档,似乎设计决策是应该有意识地释放互斥锁。如果未调用ReleaseMutex,则表示受保护区域异常退出。将释放放到finally或dispose中,就绕过了这一机制。当然,您仍然可以自由地忽略AbandonedMutexException。

请注意:垃圾收集器执行的Mutex.Dispose()失败,因为垃圾收集进程不拥有Windows的句柄。

Dispose依赖于WaitHandle。因此,即使using调用Dispose,在满足稳态条件之前也不会生效。当您调用ReleaseMutex时,您是在告诉系统您正在释放资源,因此,它可以自由地处置它。

最后一个问题

把互斥锁放在using块中有什么好处吗?或者,我应该新建一个互斥对象实例,将其包装在try/catch中,并在finally块中调用ReleaseMutex()(基本上实现了我认为Dispose()会做的事情)

如果不处理互斥对象,创建过多的互斥对象可能会遇到以下问题:

---> (Inner Exception #4) System.IO.IOException: Not enough storage is available to process this command. : 'ABCDEFGHIJK'
 at System.Threading.Mutex.CreateMutexCore(Boolean initiallyOwned, String name, Boolean& createdNew)
 at NormalizationService.Controllers.PhysicalChunkingController.Store(Chunk chunk, Stream bytes) in /usr/local/...

程序使用命名互斥,并在并行for循环中运行200,000次。
添加using语句可以解决这个问题。