【问题标题】:Need to beat the GC and have object destroyed once it goes out of scope需要击败 GC 并在对象超出范围后将其销毁
【发布时间】:2010-02-04 16:05:05
【问题描述】:

我有几段代码需要使用 Mutex 进行保护。问题是代码看起来像这样:

lock(mylockobject) {
  if(!foo())
    throw new MyException("foo failed");
  if(!bar())
    throw new MyException("bar failed");
}

使用锁,它可以按我的意愿工作,但现在我需要使用互斥锁。这里明显的问题是,如果我获取互斥锁并且 foo() 或 bar() 失败,我必须在抛出每个异常之前显式释放互斥锁。

在 C++ 中,我将利用在堆栈上创建的对象的范围,并将互斥锁锁定在对象的构造函数中,然后在析构函数中释放它。使用 .NET 的垃圾收集,我认为这行不通。我编写了一个测试应用程序并确认如果我这样做:

public class AutoMutex
{
  private Mutex _mutex;
  public AutoMutex(Mutex mutex)
  {
     _mutex = mutex;
     _mutex.WaitOne();
  }

  ~AutoMutex()
  {
    _mutex.ReleaseMutex();
  }
}

然后有这样的代码:

// some code here...
Mutex my_mutex = new Mutex(false, "MyMutex");
{ // scoping like I would do in C++
  AutoMutex test = new AutoMutex(my_mutex);
  test = null;
}

析构函数(终结器?)直到很久以后才会被调用。

Google 还没有为我指出正确的方向,但我仍在努力……如果可能的话,请告诉我你如何解决这个小问题。

【问题讨论】:

标签: c# garbage-collection locking


【解决方案1】:

几个点。

1) 您要搜索的是“一次性图案”。要非常小心地正确地实现它。当然,Mutex已经实现了一次性模式,所以我不清楚你为什么要自己制作,但仍然值得学习。

有关使用一次性模式是否明智的其他想法,请参阅此问题,就好像它是 RAII:

Is it abusive to use IDisposable and "using" as a means for getting "scoped behavior" for exception safety?

2) Try-finally 也有你想要的语义。当然,“使用”块只是 try-finally 的语法糖。

3) 你确定你想在有东西抛出时释放互斥锁吗?您确定要扔到受保护的区域内吗?

这是一种不好的代码气味,原因如下。

为什么首先要有互斥锁?通常是因为模式是这样的:

  • 状态一致但陈旧
  • 锁定对状态的访问
  • 使状态不一致
  • 使状态一致
  • 解锁对状态的访问
  • 状态现在是一致且新鲜的

考虑在“使状态一致”之前抛出异常时会发生什么。 您解锁了对现在不一致且陈旧的状态的访问权限。

保持锁定可能是一个更好的主意。是的,这意味着冒死锁的风险,但至少你的程序没有在垃圾、陈旧、不一致的状态下运行。

从受锁保护的区域内抛出异常是一件可怕的事情,您应该尽可能避免这样做。从锁内部抛出的异常使您必须在两种可怕的事情之间做出选择:要么出现死锁,要么在程序操作不一致的状态时出现疯狂的崩溃和不可重现的行为。

你真正应该实现的模式是:

  • 状态一致但陈旧
  • 锁定对状态的访问
  • 使状态不一致
  • 使状态一致
  • 如果发生异常,回滚到陈旧的一致状态
  • 解锁对状态的访问
  • 状态现在是一致的,如果没有异常,就是新鲜的

这是更安全的选择,但编写执行此类事务的代码很困难。没有人说多线程很容易。

【讨论】:

  • 解释起来有点奇怪,但我必须使用互斥锁的基本原因是我使用的是非线程安全的第 3 方 C DLL。它是一个 DLL,允许我通过通信网络发送命令,并且我编写的每个进程内服务器都需要有自己的 DLL 副本。但是,他们必须在某处使用静态,因为锁定无济于事。我能想到的唯一其他临时解决方案是像互斥锁一样进行系统范围的锁定。最后,我认为他们建议拥有单独的 DLL 副本是不必要的。作为两个进程,一切正常。
  • @Eric:+1 以获得更深入的问题答案。对于您最后描述的模式,我通常希望使用类似写时复制的方法,以简化在异常期间处理状态回滚。它还允许在互斥体外部执行大部分操作,从而提高性能。 en.wikipedia.org/wiki/Copy-on-write
【解决方案2】:

为了提供范围,您可以让您的 AutoMutex 实现 IDisposable 并像这样使用它:

using(new AutoMutex(.....))
{
  if(!foo())
    throw new MyException("foo failed");
  if(!bar())
    throw new MyException("bar failed");
}    

在您的IDisposable.Dispose() 实现中,释放互斥锁。

【讨论】:

  • 尽管每个人都提供了非常相似的建议,但 Marcel,您的答案对我来说非常有效。现在我的 AutoMutex 完全按照我的预期工作!!!谢谢!
【解决方案3】:

Mutex 实现了 IDisposable,因此将其包装在 using 中

using (Mutex m = new Mutex())
{
   //Use mutex here

}
//Mutex is out of scope and disposed

【讨论】:

  • @Oded:如果“不是”你的意思是“Mutex 没有实现IDispoable" then you are mistaken. Mutex` 继承自实现IDisposable 的WaitHandle。
  • @Oded-- 这是因为它扩展了 WaitHandle 并且 WaitHandle 实现了 IDisposable。但是,这里并没有多大用处,除非 AutoMutex 扩展 Mutex 而不是通过组合使用它。
  • 就是,Mutex扩展了Waithandle,实现了IDisposable。耶继承!
  • @tvanfosson,我认为他建议创建自己的互斥锁类,因为他不明白如何使用交付的类。
  • 这个想法不是要释放互斥锁,而是在 Dispose 上强制释放互斥锁。 Mutex Dispose 函数不会释放互斥体。
【解决方案4】:

使用 try/finally 块或使用 IDisposable 模式并将您的用法包装在 using 语句中。

【讨论】:

  • 我试试这个!好像很多人都有同样的建议。我很高兴这也让我大开眼界,现在对未来如何处理这种情况有了一个想法(即遵循 IDisposable 之类的继承路径)
【解决方案5】:

我想每个人都对 dispose/using 有所了解,但这里有一个使用 try/finally 的示例:

Mutex m = new Mutex() m.WaitOne(); try { if(..) throw new Exception(); } finally { // code in the finally will run regardless of whether and exception is thrown. m.ReleaseMutex(); }

【讨论】:

    【解决方案6】:

    GC 不是确定性的。此外,它在调试模式下的行为也有所不同,这使得在开发环境中处理起来更加混乱。

    要以您希望的方式创建和使用自动互斥锁,请实现 IDisposable 并使用 using 关键字在 AutoMutex 超出范围时销毁/释放它。

    public class AutoMutex : IDisposable
    {
        private Mutex _mutex;  
        public AutoMutex(Mutex mutex)
        {
           _mutex = mutex;
           _mutex.WaitOne();
        }
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        void Dispose(bool disposing)
        {
            // method may be called more than once from different threads
            // you should avoid exceptions in the Dispose method
            var victim = _mutex;
            _mutex = null;
            if(victim != null)
            {
              victim.ReleaseMutex();
              victim.Dispose();
            }
            if(disposing)
            {
              // release managed resources here
            }
        }
    }
    

    正在使用中

    using(new AutoMutex(new Mutex(false, "MyMutex")))
    {
      // whatever within the scope of the mutex here
    }
    

    【讨论】:

      【解决方案7】:

      为什么不将 Foo() 中的代码放在 try-catch-finally 中,然后让 finally 释放互斥锁? (catch 可以重新抛出任何异常,如果需要,可以将它们包装在您的自定义异常类型中。)

      【讨论】:

        【解决方案8】:

        对于基于范围的操作,C# 习语是try...finally。它看起来像这样:

        try {
            acquire the mutex
            do your stuff which may fail with an exception
        } finally {
            release the mutex
        }
        

        使用对象实例来获取资源是一种 C++ 主义,它源于 C++ 处理生命周期受当前范围限制的实例的能力。在实例在堆中分配并由垃圾收集器(C#、Java)异步销毁的语言中,析构函数(又名终结器)不是合适的工具。相反,finally 构造用于执行基于范围的操作。

        【讨论】:

          猜你喜欢
          • 2012-08-27
          • 1970-01-01
          • 2012-06-25
          • 2011-10-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-04-06
          • 2016-03-17
          相关资源
          最近更新 更多