What is the correct way to prevent reentrancy and

2019-04-23 14:48发布

问题:

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).

回答1:

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 :)



回答2:

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/



回答3:

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