3

I am implementing a class library and looking for a way to restrict the number of instances of a given class the library will allocate to a pre-set number. The restriction has to be machine wide - a simple static counter isn't enough as this will only count the instances in the calling process. I want to keep things as simple as possible (no memory mapped files etc) and as secure as possible (no storing counters in temp files or the registry) so decided to try using a global shared semaphore as the "counter"

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

That seems to work OK. However if Dispose() doesnt get called, the semaphore never gets released - essentially "leaking" the instance. Now IMHO IDisposable is one of the the worst parts of .NET - I see far more code missing using( ... ) {} than with it. Even worse when you use a IDisposable as a data member and watch as "IDisposable cancer" spreads though every class in your app.

So I decide to implement the full IDisposable (anti-)pattern for people who forget the using()'s.

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

Things are simple when being called correctly with using, I release the semapore. But when being called as a last resort by the finalize, as far as I understand it I should not access managed resources as the destruction order isn't fixed - m_sem may have been destroyed.

So, how can I release the semaphore in the case where the user forgets using? (RTFM may be a valid answer, but one I hope to avoid). As it stands, the "leaked" instance is counted right up until the final process that is using my assembly terminates (at which point I assume the global semaphore is freed)

Or indeed is there a better way of doing this?

James Hobson
  • 193
  • 2
  • 8
  • After much reading, even MSs own code seems inconsistent in this matter. Sometimes examples never call `Dispose()` on `IDisposable` data members when being called from finalizers, sometimes they do. It all seems based on MSs own knowledge of what the contained class is doing under the hood - a thin managed wrapper around an unmanaged is safe, othewise not. Yet another reason to hate IDisposable I suppose :). – James Hobson Oct 17 '13 at 06:40
  • You are completely wrong with your assumptions about IDisposable. This interface allows the programmer to free the resources of an instance MANUALLY. (e.g. recommnded for Bitmaps which use lot of memory) But calling Dispose() is an OPTION. If you don't call it in your code the garbage collector will free the instance later alone. So it is wrong what you write about the semaphore never getting released. It WILL be released when the garbage collector disposes it. EVEERYTHING in .NET which is not used anymore will be collected by the garbage collector. But it may take a while. – Elmue Apr 10 '21 at 16:02

2 Answers2

2

Sometimes, RTFM really is the answer.

I wouldn't necessarily recommend this, but one thing you can do is pin the semaphore object. For example:

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

I wouldn't do this if I had a whole bunch of these objects because pinning objects can have a negative impact on garbage collector efficiency. But as I understand it, a Normal pinned object isn't a problem (or not as much of a problem).

All things considered, I think I'd prefer the RTFM approach.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • 1
    how does this address the question of ensuring that the semaphore is released when `using` is not used (or Dispose is otherwise not called)? – Peter Ritchie Oct 16 '13 at 19:42
  • @PeterRitchie: I'm assuming that the OP will incorporate the `GCHandle` trick in his full implementation of the Dispose pattern, which would allow the semaphore to be disposed in the finalizer (actually, when the finalizer calls `Dispose`). – Jim Mischel Oct 16 '13 at 20:38
  • @JimMischel Actually not a bad idea :). I think as you say, RTFM may be better. – James Hobson Oct 17 '13 at 06:37
0

The semaphore will never get GC'd before your instance of MyClass gets garbage collected, because your instance of MyClass still has a reference to it. The GC won't collect something if there's a reference to it. Once your instance of MyClass is properly finalized (the ~MyClass() function returned without an error), it no longer has a reference to the semaphore and then GC will collect that.

The only concern would be WHEN garbage collection hits, as it's non-deterministic (and therefore may never run). But you can't do anything about that.

Also, make sure to have a blanket catch (Exception e) clause in there. Exceptions on the GC thread can cause some funky stuff.

M.S.
  • 71
  • 3
  • 1
    That's not entirely true. The semaphore instance can be collected at any time after the `MyClass` instance goes onto the finalization queue. So it's quite possible for `m_sem` to be collected before the finalizer runs. – Jim Mischel Oct 16 '13 at 19:35
  • This really just describes the problem, not a solution (answer). i.e. when the object is GC'd it still won't dispose and thus won't release the semaphore – Peter Ritchie Oct 16 '13 at 19:37