What is the correct way to prevent reentrancy and ensure a lock is acquired for certain operations?
Asked Answered
D

4

12

I'm designing a base class that, when inherited, will provide business functionality against a context in a multithreaded environment. Each instance may have long-running initialization operations, so I want to make the objects reusable. In order to do so, I need to be able to:

  1. Assign a context to one of these objects to allow it to do its work
  2. Prevent an object from being assigned a new context while it already has one
  3. Prevent certain members from being accessed while the object doesn't have a context

Also, each context object can be shared by many worker objects.

Is there a correct synchronization primitive that fits what I'm trying to do? This is the pattern I've come up with that best fits what I need:

private Context currentContext;

internal void BeginProcess(Context currentContext)
{
    // attempt to acquire a lock; throw if the lock is already acquired,
    // otherwise store the current context in the instance field
}

internal void EndProcess()
{
    // release the lock and set the instance field to null
}

private void ThrowIfNotProcessing()
{
    // throw if this method is called while there is no lock acquired
}

Using the above, I can protect base class properties and methods that shouldn't be accessed unless the object is currently in the processing state.

protected Context CurrentContext
{
    get
    {
        this.ThrowIfNotProcessing();
        return this.context;
    }
}

protected void SomeAction()
{
    this.ThrowIfNotProcessing();

    // do something important
}

My initial though was to use Monitor.Enter and related functions, but that doesn't prevent same-thread reentrancy (multiple calls to BeginProcess on the original thread).

Dour answered 25/9, 2013 at 23:2 Comment(1)
Since you already throw if the lock is already acquired and don't want to wait for current processing to finish: Why not just throw if current context is not null?Stoned
P
14

There is one synchronization object in .NET that isn't re-entrant, you are looking for a Semaphore.

Before you commit to this, do get your ducks in a row and ask yourself how it can be possible that BeginProcess() can be called again on the same thread. That is very, very unusual, your code has to be re-entrant for that to happen. This can normally only happen on a thread that has a dispatcher loop, the UI thread of a GUI app is a common example. If this is truly possible and you actually use a Semaphore then you'll get to deal with the consequence as well, your code will deadlock. Since it recursed into BeginProcess and stalls on the semaphore. Thus never completing and never able to call EndProcess(). There's a good reason why Monitor and Mutex are re-entrant :)

Priapic answered 26/9, 2013 at 1:39 Comment(2)
You bring up good points; the main function of this is to ensure my infrastructure code that calls into BeginProcess is doing so correctly. "A good programmer looks both ways before crossing a one-way street." I won't allow the semaphore to block (I'll call WaitOne with a zero timeout) and check the return value to decide whether or not to throw. This will prevent a deadlock scenario.Dour
This answer smells experience!Sikorsky
S
11

You can use Semaphore class which came with .NET Framework 2.0.

A good usage of Semaphores is to synchronize limited amount of resources. In your case it seems you have resources like Context which you want to share between consumers.

You can create a semaphore to manage the resources like:

var resourceManager = new Semaphore(0, 10);

And then wait for a resource to be available in the BeginProcess method using:

resourceManager.WaitOne();

And finally free the resource in the EndProcess method using:

resourceManager.Release();

Here's a good blog about using Semaphores in a situation like yours:

https://web.archive.org/web/20121207180440/http://www.dijksterhuis.org/using-semaphores-in-c/

Sikorsky answered 26/9, 2013 at 1:45 Comment(1)
the links you provided are broken!Tinney
P
0

The Interlocked class can be used for a thread-safe solution that exits the method, instead of blocking when a re-entrant call is made. Like Vlad Gonchar solution, but thread-safe.


    private int refreshCount = 0;
    private void Refresh()
    {
      if (Interlocked.Increment(ref refreshCount) != 1) return;
        
      try
      {
        // do something here
      }
      finally
      {
        Interlocked.Decrement(ref refreshCount);
      }
    }

Peerless answered 9/12, 2021 at 14:27 Comment(0)
I
-2

There is very simple way to prevent re-entrancy (on one thread):

private bool bRefresh = false;
private void Refresh()
{
  if (bRefresh) return;
  bRefresh = true;
  try
  {
    // do something here
  }
  finally
  {
    bRefresh = false;
  }
}
Immunoreaction answered 28/9, 2014 at 11:51 Comment(3)
That absolutely does not prevent reentrancy.Donofrio
It doesn't prevent it because if (bRefresh) return; together with bRefresh = true; is not an atomic operation, so multiple threads could evaluate the condition while bRefresh is false, skip the return and then go on to setting it to true and doing the rest of the work.Forgave
@Forgave Of course it does prevent reëntrancy - it just isn't thread-safe. The two concepts are entirely unrelated - two threads accessing the same resource has nothing to do with reëntrancy. If you need to prevent both reëntrancy and simultaneous access from multiple threads, you need to combine both approaches or use a primitive that considers both.Shien

© 2022 - 2024 — McMap. All rights reserved.