为什么在释放Mutex的时候,Mutex没有被释放?

61

I have the following code:

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块终止时,第二个进程会抛出关于未释放Mutex的异常。

解决方法是调用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块内有多个等待和释放,但是当处理Mutex的句柄被处理时,它不应该自动释放吗?换句话说,在using块中,如果处理Mutex的句柄被处理,为什么还需要调用ReleaseMutex?我现在也担心,如果if块内的代码崩溃,那么会有未释放的mutex。
将Mutex放入using块中是否有任何好处?或者,我只需新建一个Mutex实例,将其包装在try / catch中,并在finally块中调用ReleaseMutex()(基本上实现了我认为Dispose()会做的事情)?

了解互斥锁的概念后,我想当 Dispose() 方法被调用时,它会检查互斥锁是否仍在使用。如果是这样,我认为它不会将其自身处理掉(也不应该这样做!)然而,我希望从更熟悉 .Net 中 Mutex 类内部的人那里获得关于此主题的进一步信息。 - RLH
using 的目的只是在结束时调用 Dispose。它被转换为 try{ //yourcode } finally { yourobj.Dispose() }... 如果你需要更多的逻辑(比如在这种情况下释放),你需要自己管理。 - pollirrata
你是在多个进程中使用它,还是只关注同一进程内的多个线程? - voithos
@voithos - 多个进程,这就是 Mutex 有名称的原因。 - Mike Christensen
10个回答

64

文档(“Remarks”部分)说明,实例化 Mutex 对象与 获取 Mutex(使用 WaitOne)之间存在概念上的区别,前者在同步方面实际上并不做任何特殊处理。请注意以下几点:

  • WaitOne 返回布尔值,表示获取 Mutex 可能会失败(超时),必须处理这两种情况
  • WaitOne 返回 true 时,调用线程已经获取了 Mutex,并且必须调用 ReleaseMutex,否则 Mutex 将变为废弃状态
  • 当它返回 false 时,调用线程不得调用 ReleaseMutex

因此,Mutex 不仅仅是实例化。至于是否应该仍然使用 using,让我们来看一下 Dispose 的作用(从 WaitHandle 继承而来):

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

正如我们所看到的,Mutex没有被释放,但是有一些清理工作需要完成,因此坚持使用using是一个不错的方法。

至于如何继续,当然可以使用try/finally块来确保如果获得了Mutex,它会被正确地释放。这可能是最直接的方法。

如果你真的不关心Mutex无法获取的情况(因为你传递了TimeSpanWaitOne),你可以将Mutex封装在自己的实现IDisposable的类中,在构造函数中获取Mutex(使用不带参数的WaitOne()),并在Dispose中释放它。虽然我可能不建议这样做,因为如果出现问题,这会导致您的线程无限等待,并且无论如何,在尝试获取时明确处理两种情况都有充分的理由,正如@HansPassant所提到的那样。


7
+1 - 很棒的回答!我认为如果 WaitOne 失败,则无法调用 ReleaseMutex 的事实意味着 Dispose 不能实现这一点,除非它还实现了跟踪成功等待的逻辑。看起来解决方案是将 Mutex 放入 using 块中,并在 if 块内部放置 try/finally 块。 - Mike Christensen
2
@MikeChristensen 如果在超时时间内未能获取互斥锁,它可以直接throw异常,这样更有意义,因为它表示了一个失败。 - Benjamin Gruenbaum
如果您无法调用ReleaseMutex,则可以将其放在dispose函数的 try {} 中。 - 00jt

41

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

注意WaitForSingleObject()返回值中的WAIT_ABANDONED,这是产生异常的原因。

Windows设计人员制定了固定规则:拥有互斥锁的线程必须在退出之前调用ReleaseMutex()。如果没有这样做,这非常强烈地表明线程以异常方式终止。这意味着同步丢失,这是一个非常严重的线程错误。与Thread.Abort()相比,后者也是.NET中终止线程的一种非常危险的方式,因为原因相同。

.NET设计人员没有以任何方式改变这种行为。主要因为没有任何方法可以测试互斥锁的状态,除了执行等待。你必须调用ReleaseMutex()。注意,第二个代码片段也不正确;你不能在未获取互斥锁的情况下调用它。它必须放到if()语句体内。


+1 - 感谢您提供的历史信息!您能否对我在回答中发布的代码进行评论?据我所知,这应该是完成此操作的理想方式。 - Mike Christensen
1
哎呀,你正在故意掩盖Windows设计师特意构建的诊断功能。你的代码在拥有互斥锁的情况下崩溃了,你无法再推理程序的状态了。你能够恢复的可能性极小,特别是因为你没有其他好的方法来通知另一个进程停止使用共享资源,因为它处于完全未知的状态。异常是一件好事,请不要这样做。 - Hans Passant
你说.NET的设计师们没有改变任何东西,但我似乎记得从1.1到2.0版本中WaitXXX抛出的AbandonedMutexException是一个破坏性的变化。在1.1(和1.0)中,WaitXXX方法包装器只是在遇到废弃的互斥锁时返回。然后就会出现很多有趣的问题。 - Damien_The_Unbeliever
@Damien_The_Unbeliever:锁原语具有“等待条件永远不会发生”的状态概念非常有用。我希望监视器锁包括类似的东西,但我不确定如果有人将其锁定到这种状态,现有代码可以期望做什么。Monitor.Pulse只能在获取锁之后执行是有充分理由的,但应该有一种机制,使关闭服务的代码可以在不必先获取相关锁的情况下将其监视器设置为“关闭”状态。如果有类似终结器的东西... - supercat
这个答案需要被纳入到“Mutex”的文档中。截至目前(2017年3月),该行为似乎没有被记录在文档中。 - CoderBrien
显示剩余4条评论

8

好的,我来回答自己的问题。据我所知,这种方法是实现Mutex的理想方式,具体要求如下:

  1. 始终处于已释放状态。
  2. 只有在成功执行WaitOne时才会被释放。
  3. 不会因为任何代码抛出异常而被弃置。

希望对大家有所帮助!

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

更新:有人可能会认为,如果try块中的代码使您的资源处于不稳定状态,则应该释放Mutex,而是让它被放弃。换句话说,在代码成功完成时,只需调用mut.ReleaseMutex();,并不将其放在finally块中。获取Mutex的代码可以捕获此异常并做正确的事情

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


不确定为什么这个被踩了,但我已经添加了更多细节。也许回答自己的问题只是被认为是不好的形式 - Mike Christensen
1
回答自己的问题并不是什么坏习惯!它可能被 downvote 是因为代码有误。你正在捕获和忽略异常,这会隐藏一个严重的线程 bug——这种 bug 是你不想要的。 - Cody Gray
2
@CodyGray,我看到Mike在你的评论后添加了一个catch子句。但是,没有catch(){}的try/finally块可以吗:异常可能会在堆栈上方处理,而不是被吞噬?(如果根本没有捕获,程序将终止,释放互斥锁并不重要!) - Darren Cook

7
互斥锁的主要用途之一是确保只有那些(希望暂时)将对象置于该状态的代码才能看到共享对象不满足其不变式的状态。需要修改对象的代码通常会遵循以下模式:
  1. 获取互斥锁
  2. 更改对象,使其状态无效
  3. 更改对象,使其状态再次有效
  4. 释放互斥锁
如果在第2步开始后第3步完成前出现问题,则可能会导致对象处于不满足其不变式的状态。由于正确的模式是在处理完互斥锁之后释放它,因此对互斥锁进行处理而未释放它意味着某些地方出现了问题。因此,代码进入互斥锁可能不安全(因为它没有被释放),但没有理由等待互斥锁被释放(因为已经被处理--它永远不会被释放)。因此,正确的做法是抛出异常。
.NET互斥锁对象所实现的模式比较友好,其“获取”方法返回一个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块中的代码将不再需要访问它。我没有关于实现该模式的库的特别建议,但将其作为其他类型互斥体的包装器实现并不特别困难。

4
我们需要了解更多的内容才能知道MSDN页面开头发出的第一个暗示,表明某些“奇怪”的事情正在发生:
一个同步原语,也可以用于进程间同步。
Mutex是一个Win32“命名对象”,每个进程通过名称锁定它,.net对象只是Win32调用的包装器。Muxtex本身存在于Windows内核地址空间中,而不是应用程序地址空间中。
在大多数情况下,如果您只想同步访问单个进程中的对象,则最好使用监视器

3

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


你是什么意思?这是否意味着using语句与try/finally{Dispose()}不同? - Sriram Sakthivel
1
@SriramSakthivel,不是指你只需使用Dispose(由using语句完成)就可以,而是需要使用try/finally调用ReleaseMutex - CMircea
1
这个解决方案行不通,因为如果WaitOne调用超时,它会在finally代码块中抛出异常。 - Mike Christensen

2

注意:根据Windows的规定,由垃圾回收进程执行的Mutex.Dispose()会失败,因为垃圾回收进程不拥有该句柄。


2

阅读ReleaseMutex的文档,似乎设计决策是Mutex应该被有意识地释放。如果没有调用ReleaseMutex,则表示受保护部分发生异常退出。把释放操作放在finally或dispose中可以规避这种情况。当然,您仍然可以选择忽略AbandonedMutexException。


1

针对最后一个问题。

将Mutex放在using块中是否有益处?还是说我只需要新建一个Mutex实例,在try/catch语句中包装它,在finally块中调用ReleaseMutex()(基本上实现了我认为Dispose()会做的事情)

如果您不处理mutex对象,创建太多的mutex对象可能会遇到以下问题。

---> (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/...

该程序使用命名互斥锁,在并行循环中运行20万次。
添加using语句解决了这个问题。

1

Dispose 的释放取决于 WaitHandle。因此,即使使用 using 调用了 Dispose,但只有当稳定状态的条件满足时,它才会生效。当您调用 ReleaseMutex 时,您告诉系统已释放资源,因此可以进行处理。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接