【问题标题】:Global class instance counting (with Semaphores)全局类实例计数(使用信号量)
【发布时间】:2013-10-17 04:08:51
【问题描述】:

我正在实现一个类库,并正在寻找一种方法来限制该库将分配给预设数量的给定类的实例数。限制必须是机器范围 - 一个简单的static 计数器是不够的,因为它只会计算调用进程中的实例。我想让事情尽可能简单(没有内存映射文件等)和尽可能安全(没有在临时文件或注册表中存储计数器),所以决定尝试使用全局共享信号量作为“计数器”

public class MyClass : IDisposable
{
   // Limit to 10 instances
   Semaphore m_sem = new Semaphore(10, 10, "SharedName");

   public MyClass()
   {
     if(!m_sem.WaitOne(0))
     {
       throw new Exception("No instances free");
     }
   }

   void IDisposable.Dispose()
   {
     m_sem.Release();
   }
}

这似乎工作正常。但是,如果 Dispose() 没有被调用,则信号量永远不会被释放 - 本质上是“泄漏”实例。现在恕我直言IDisposable 是.NET 中最糟糕的部分之一——我看到using( ... ) {} 的代码丢失比使用它要多得多。更糟糕的是,当您使用 IDisposable 作为数据成员并观察“IDisposable 癌症”在您应用中的每个类中蔓延时。

所以我决定为忘记 using() 的人实现完整的 IDisposable(反)模式。

public class MyClass : IDisposable
{
   // Limit to 10 instances
   Semaphore m_sem = new Semaphore(10, 10, "SharedName");

   public MyClass()
   {
     if(!m_sem.WaitOne(0))
     {
       throw new Exception("No instances left");
     }
   }

   ~MyClass()
   {
      Dispose(false);
   }

   void IDisposable.Dispose()
   {
     Dispose(true);
     GC.SuppressFinalize(this);
   }

   void Dispose(bool disposing)
   {
      if(disposing)
      {
          m_sem.Release();
      }
      else
      {
      // To release or not release?
      // m_sem.Release();
      }
   }
}

当使用using 正确调用时,事情很简单,我释放了 semapore。但是,当被 finalize 作为最后手段调用时,据我了解,我不应该访问托管资源,因为销毁顺序不固定 - m_sem 可能已被销毁。

那么,在用户忘记using的情况下,如何释放信号量呢? (RTFM 可能是一个有效的答案,但我希望避免)。就目前而言,“泄露”的实例一直计数到使用我的程序集的最终进程终止(此时我假设全局信号量被释放)

或者确实有更好的方法来做到这一点?

【问题讨论】:

  • 经过大量阅读,即使是MS自己的代码在这件事上似乎也不一致。有时,当从终结器调用时,示例永远不会在 IDisposable 数据成员上调用 Dispose(),有时它们会这样做。这一切似乎都基于 MS 自己对包含的类在幕后所做的事情的了解——围绕非托管的瘦托管包装是安全的,否则不是。我想还有另一个讨厌 IDisposable 的原因:)。
  • 您对 IDisposable 的假设完全错误。该接口允许程序员手动释放实例的资源。 (例如,推荐用于使用大量内存的位图)但调用 Dispose() 是一个选项。如果您不在代码中调用它,垃圾收集器稍后将单独释放该实例。所以你写的关于信号量永远不会被释放的内容是错误的。当垃圾收集器处理它时,它将被释放。 .NET 中不再使用的所有内容都将由垃圾收集器收集。但这可能需要一段时间。

标签: c# multithreading


【解决方案1】:

有时,RTFM 确实是答案。

我不一定会推荐这个,但您可以做的一件事是固定信号量对象。例如:

public class MyClass : IDisposable
{
   // Limit to 10 instances
   Semaphore m_sem = new Semaphore(10, 10, "SharedName");
   private readonly GCHandle semHandle = GCHandle.Alloc(m_sem);

   public MyClass()
   {
     if(!m_sem.WaitOne(0))
     {
       throw new Exception("No instances free");
     }
   }

   void IDisposable.Dispose()
   {
     semHandle.Free();
     m_sem.Release();
   }
}

如果我有一大堆这些对象,我不会这样做,因为固定对象会对垃圾收集器的效率产生负面影响。但据我了解,Normal 固定对象不是问题(或问题不大)。

综合考虑,我认为我更喜欢 RTFM 方法。

【讨论】:

  • 这如何解决确保在不使用using 时释放信号量的问题(或者不调用 Dispose)?
  • @PeterRitchie:我假设 OP 将在他的 Dispose 模式的完整实现中包含GCHandle 技巧,这将允许在终结器中处理信号量(实际上,当终结器调用Dispose)。
  • @JimMischel 实际上不是一个坏主意:)。我认为正如您所说,RTFM 可能会更好。
【解决方案2】:

在您的 MyClass 实例被垃圾回收之前,信号量永远不会被 GC,因为您的 MyClass 实例仍然具有对它的引用。如果有对它的引用,GC 将不会收集它。一旦您的 MyClass 实例正确完成(~MyClass() 函数返回时没有错误),它不再具有对信号量的引用,然后 GC 将收集它。

唯一的问题是垃圾收集何时命中,因为它是不确定的(因此可能永远不会运行)。但你对此无能为力。

另外,请确保在其中包含 catch (Exception e) 子句。 GC 线程上的异常会导致一些奇怪的东西。

【讨论】:

  • 这并不完全正确。在MyClass 实例进入终结队列后,可以随时收集信号量实例。所以m_sem 很有可能在终结器运行之前被收集。
  • 这实际上只是描述了问题,而不是解决方案(答案)。即当对象被 GC 处理时,它仍然不会释放,因此不会释放信号量
猜你喜欢
  • 1970-01-01
  • 2023-03-24
  • 1970-01-01
  • 2017-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-22
  • 2013-06-12
相关资源
最近更新 更多