简体   繁体   中英

Resource locking between iterations of the main thread (Async/Await)

Let's say I have a form with two buttons ( button1 and button2 ) and a resource object ( r ). The resource has its own locking and unlocking code to handle concurrency. The resource could be modified by any thread.

When button1 is clicked, its handler does some modifying of r itself and then calls _IndependentResourceModifierAsync() asynchronously which does some modifying of r in a spawned task. _IndependentResourceModifierAsync() acquires r 's lock before doing this. Also because the handler is messing with r itself, it acquires r 's lock too.

When button2 is clicked, it just calls _IndependentResourceModifierAsync() directly. It does no locking itself.

As you know, the handlers for the buttons will always execute on the main thread (except for the spawned Task ).

There's two things that I want to guarantee:

  1. If either button1 or button2 is clicked while the resource is locked by the main thread, an exception will be thrown. (Can't use a Monitor or Mutex because they are thread driven)
  2. The nesting of locks from button1_Click() through _IndependentResourceModiferAsync() should not cause a deadlock. (Can't use a Semaphore ).

Basically, I think what I'm looking for is a "stack-based lock" if such a thing exists or is even possible. Because when an async method continues after an await, it restores stack state. I did a lot of searching for anyone else who has had this problem but came up dry. That likely means I'm over-complicating things, but I am curious what people have to say about it. There might be something really obvious I'm missing. Many thanks.

public class Resource
{
    public bool TryLock();
    public void Lock();
    public void Unlock();
    ...
}

public class MainForm : Form
{
    private Resource r;
    private async void button1_Click(object sender, EventArgs e)
    {
        if (!r.TryLock())
            throw InvalidOperationException("Resource already acquired");
        try
        {
            //Mess with r here... then call another procedure that messes with r independently.
            await _IndependentResourceModiferAsync();
        }
        finally
        {
            r.Unlock();
        }
    }

    private async void button2_Click(object sender, EventArgs e)
    {
        await _IndependentResourceModifierAsync();
    }

    private async void _IndependentResourceModiferAsync()
    {
        //This procedure needs to check the lock too because he can be called independently
        if (!r.TryLock())
            throw InvalidOperationException("Resource already acquired");
            try
            {
                await Task.Factory.StartNew(new Action(() => {
                    // Mess around with R for a long time.
                }));
            }
            finally
            {
                r.Unlock();
            }
    }
}

I believe asynchronous re-entrant locking that behaves reasonably well is impossible. This is because when you start an asynchronous operation, you're not required to immediately await it.

For example, imagine you changed your event handler to something like this:

private async void button1_Click(object sender, EventArgs e)
{
    if (!r.TryLock())
        throw InvalidOperationException("Resource already acquired");
    try
    {
        var task = _IndependentResourceModiferAsync();
        // Mess with r here
        await task;
    }
    finally
    {
        r.Unlock();
    }
}

If the lock was asynchronously re-entrant, the code that works with r in the event handler and the code in the invoked asynchronous method could work at the same time (because they can run on different threads). This means such lock wouldn't be safe.

The resource has its own locking and unlocking code to handle concurrency. The resource could be modified by any thread.

There's a yellow flag. I find that a design where you protect resources (rather than have them protect themselves) is usually better in the long run.

When button1 is clicked, its handler does some modifying of r itself and then calls _IndependentResourceModifierAsync() asynchronously which does some modifying of r in a spawned task. _IndependentResourceModifierAsync() acquires r's lock before doing this. Also because the handler is messing with r itself, it acquires r's lock too.

And there's a red flag. Recursive locks are almost always a bad idea. I explain my reasoning on my blog.

There's also another warning I picked up regarding the design:

If either button1 or button2 is clicked while the resource is locked by the main thread, an exception will be thrown. (Can't use a Monitor or Mutex because they are thread driven)

That doesn't sound right to me. Is there any other way to do this? Disabling buttons as the state changes seems like a much nicer approach.


I strongly recommend refactoring to remove the requirement for lock recursion. Then you can use SemaphoreSlim with WaitAsync to asynchronously acquire the lock and Wait(0) for the "try-lock".

So your code would end up looking something like this:

class Resource
{
  private readonly SemaphoreSlim mutex = new SemaphoreSlim(1);

  // Take the lock immediately, throwing an exception if it isn't available.
  public IDisposable ImmediateLock()
  {
    if (!mutex.Wait(0))
      throw new InvalidOperationException("Cannot acquire resource");
    return new AnonymousDisposable(() => mutex.Release());
  }

  // Take the lock asynchronously.
  public async Task<IDisposable> LockAsync()
  {
    await mutex.WaitAsync();
    return new AnonymousDisposable(() => mutex.Release());
  }
}

async void button1Click(..)
{
  using (r.ImmediateLock())
  {
    ... // mess with r
    await _IndependentResourceModiferUnsafeAsync();
  }
}

async void button2Click(..)
{
  using (r.ImmediateLock())
  {
    await _IndependentResourceModiferUnsafeAsync();
  }
}

async Task _IndependentResourceModiferAsync()
{
  using (await r.LockAsync())
  {
    await _IndependentResourceModiferUnsafeAsync();
  }
}

async Task _IndependentResourceModiferUnsafeAsync()
{
  ... // code here assumes it owns the resource lock
}

I did a lot of searching for anyone else who has had this problem but came up dry. That likely means I'm over-complicating things, but I am curious what people have to say about it.

For a long time, it wasn't possible (at all, period, full-stop). With .NET 4.5, it is possible, but it's not pretty. It's very complicated. I'm not aware of anyone actually doing this in production, and I certainly don't recommend it.

That said, I have been playing around with asynchronous recursive locks as an example in my AsyncEx library (it will never be part of the public API). You can use it like this (following the AsyncEx convention of already-cancelled tokens acting synchronously ):

class Resource
{
  private readonly RecursiveAsyncLock mutex = new RecursiveAsyncLock();
  public RecursiveLockAsync.RecursiveLockAwaitable LockAsync(bool immediate = false)
  {
    if (immediate)
      return mutex.LockAsync(new CancellationToken(true));
    return mutex.LockAsync();
  }
}

async void button1Click(..)
{
  using (r.LockAsync(true))
  {
    ... // mess with r
    await _IndependentResourceModiferAsync();
  }
}

async void button2Click(..)
{
  using (r.LockAsync(true))
  {
    await _IndependentResourceModiferAsync();
  }
}

async Task _IndependentResourceModiferAsync()
{
  using (await r.LockAsync())
  {
    ...
  }
}

The code for RecursiveAsyncLock is not very long but it is terribly mind-bending to think about. It starts with the implicit async context that I describe in detail on my blog (which is hard to understand just by itself) and then uses custom awaitables to "inject" code at just the right time in the end-user async methods.

You're right at the edge of what anyone has experimented with. RecursiveAsyncLock is not thoroughly tested at all, and likely never will be.

Tread carefully, explorer. Here be dragons.

I think you should look at SemaphoreSlim (with a count of 1):

  • It isn't re-entrant (it's not owned by a thread)
  • It supports asynchronous waiting ( WaitAsync )

I don't have time to check through your scenario right now, but I think it would fit.

EDIT: I've just noticed this bit of the question:

Because when an async method continues after an await, it restores stack state.

No, it absolutely doesn't. That's easily to show - add an async method which responds to a button click like this:

public void HandleClick(object sender, EventArgs e)
{
    Console.WriteLine("Before");
    await Task.Delay(1000);
    Console.WriteLine("After");
}

Set a break point on both of your Console.WriteLine calls - you'll notice that before the await , you've got a stack trace including the "button handling" code in WinForms; afterwards the stack will look very different.

Keep in mind that many people have told you to refactor away from lock reentrance. There's a lot of wisdom in that. BUT if you don't want to do that then check out this NuGet package that I wrote:

https://www.nuget.org/packages/ReentrantAsyncLock/

This is the only correct async equivalent to Monitor.Enter / lock that I know of. It is the only thing I've seen that gives you all three of these at the same time:

  • Asynchronicity
  • Reentrance
  • Mutual exclusion

You'll notice that the second two of those are what Monitor.Enter / lock gives you. I think it's valid to call ReentrantAsyncLock the async equivalent.

I believe this is how it would look in your code:

public class MainForm : Form
{
    readonly ReentrantAsyncLock r = new();
    private async void button1_Click(object sender, EventArgs e)
    {
        await using (await r.LockAsync(CancellationToken.None))
        {
            //Mess with r here... then call another procedure that messes with r independently.
            await _IndependentResourceModiferAsync();
        }
    }

    private async void button2_Click(object sender, EventArgs e)
    {
        await _IndependentResourceModifierAsync();
    }

    private async void _IndependentResourceModiferAsync()
    {
        //This procedure needs to check the lock too because he can be called independently
        await using (await r.LockAsync(CancellationToken.None))
        {
            await Task.Factory.StartNew(new Action(() => {
                // Mess around with R for a long time.
            }));
        }
    }
}

Here's a demonstration of it in the general sense (copied from the package documentation):

var asyncLock = new ReentrantAsyncLock();
var raceCondition = 0;
// You can acquire the lock asynchronously
await using (await asyncLock.LockAsync(CancellationToken.None))
{
    await Task.WhenAll(
        Task.Run(async () =>
        {
            // The lock is reentrant
            await using (await asyncLock.LockAsync(CancellationToken.None))
            {
                // The lock provides mutual exclusion
                raceCondition++;
            }
        }),
        Task.Run(async () =>
        {
            await using (await asyncLock.LockAsync(CancellationToken.None))
            {
                raceCondition++;
            }
        })
    );
}
Assert.Equal(2, raceCondition);

This is certainly not the first attempt at doing this. But like I said it's the only correct attempt that I've seen so far. Some other implementations will deadlock trying to re-enter the lock in one of the Task.Run calls. Others will not actually provide mutual exclusion and the raceCondition variable will sometimes equal 1 instead of 2:

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM