2

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();
            }
    }
}
Frank Weindel
  • 1,212
  • 1
  • 13
  • 31
  • "Thing 1" will never happen. – H H Apr 24 '13 at 13:50
  • @HenkHolterman: Why not? The resource can be "owned" without blocking the UI thread, due to using async. – Jon Skeet Apr 24 '13 at 13:52
  • Yes, i missed the await. But it seems weird to use that inside a lock. – H H Apr 24 '13 at 13:56
  • The reason I use the `await Task` in a lock is because I need the GUI to know right away whether it can perform the operation or not. If it can't I might display an error message and ask them to try when the last operation has completed. – Frank Weindel Apr 24 '13 at 14:06
  • 2
    Do you really need nested locking? I believe it's generally considered a bad practice. Why don't you remove the locking `_IndependentResourceModiferAsync()` and let the called handle that? – svick Apr 24 '13 at 14:26
  • I like to keep things self-contained when at all possible. That certainly would solve #2 and let me use a `Semaphore` but for the sake of this post, let's say that is undesirable. – Frank Weindel Apr 24 '13 at 14:29

3 Answers3

4

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.

svick
  • 236,525
  • 50
  • 385
  • 514
  • 1
    +1 for some good points, but the same can really be said of any async-compatible lock. You don't have to `await` the `Task` returned by `SemaphoreSlim.WaitAsync` either. – Stephen Cleary Apr 25 '13 at 00:26
  • @StephenCleary What I meant is that if you take non-re-entrant lock and then execute “unsafe” (without any locking) method in parallel with some other code, it's your fault that the code is now buggy. But if you take re-entrant lock and then execute “safe” (with its own locking) method in parallel with something else, there might be some expectation for this to work, but it's actually just as buggy. – svick Apr 25 '13 at 10:08
4

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks a bunch for the in-depth answer of why I shouldn't be doing this, what I should do instead and how I could do it anyway (especially this). I'm going to go with something like you suggested. I really like what you're doing with an `IDisposable` lock and `using` it. This is the first time I've seen this pattern. – Frank Weindel Apr 25 '13 at 19:29
  • Quick question about `AnonymousDisposable`. I get the gist on how it works and how I could easily write it, but I'm assuming this isn't part of the .NET framework? The only "documentation" I see on it through google is StackOverflow answers. Cheers. – Frank Weindel Apr 25 '13 at 19:30
  • Right, `AnonymousDisposable` isn't a provided type. It's really easy to write, though, and I called it `AnonymousDisposable` in my answer because that's a common name for it. If you want the best performance, you should have an actual disposable type. – Stephen Cleary Apr 25 '13 at 20:41
3

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thanks Jon. I thought about a semaphore but it doesn't work when there is nested locks. – Frank Weindel Apr 24 '13 at 14:00
  • 1
    What do you mean by "it doesn't work"? It sounds like basically you should avoid the nested locks to start with... – Jon Skeet Apr 24 '13 at 14:31
  • 1
    Avoiding nested locks would let me use a semaphore. However, I like to keep locks self-contained whenever possible so the caller doesn't have to worry about it. I think this is reasonable. I'm OK with the answer "not possible". – Frank Weindel Apr 24 '13 at 14:50
  • @FrankWeindel: But the lock *isn't* self-contained now, by the looks of it. I would try to avoid the nested lock for simple sanity... both nested locking and async are complicated enough on their own, let alone combined. But I've just noticed one point of confusion which may explain things a bit - editing my answer now. – Jon Skeet Apr 24 '13 at 14:52
  • It's self-contained within `_IndependentResourceModiferAsync()`. So anyone who calls it won't have to worry about any locks. The reason `button1_Click()` locks it too is because it does something with the resource before/after the call to `_IndependentResourceModiferAsync()`. And it wants to maintain the atomicity. But I agree, these two concepts together really over-complicate things and I'm better off just forcing the ultimate callers to grab the lock. – Frank Weindel Apr 24 '13 at 14:58
  • Also when I say "stack state" I mean the state that follows the originally called async method. You'll agree if you have a bunch of async methods that call each-other with awaits that they ultimately return back to the root async method. – Frank Weindel Apr 24 '13 at 15:00
  • @FrankWeindel: It depends on exactly what you mean by "return" and what you mean by "state" - you'd have to give a concrete example for me to say yes or no, really. – Jon Skeet Apr 24 '13 at 15:05