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 lock and unlock code to handle concurrency. A resource can be changed by any thread.

When button1 pressed, its handler performs some modification of r , and then asynchronously calls _IndependentResourceModifierAsync() , which performs some modification of r in the task created to generate it. _IndependentResourceModifierAsync() locks the r lock before this. In addition, since the handler tinkers with r itself, it also locks the lock on r .

When button2 pressed, it simply calls _IndependentResourceModifierAsync() directly. He does not block himself.

As you know, button handlers will always be executed in the main thread (except for the generated Task ).

There are two things I want to guarantee:

  • If you click either button1 or button2 when the resource is locked by the main thread, an exception is thrown. (You cannot use Monitor or Mutex because they are flow controlled)
  • button1_Click() locks from button1_Click() to _IndependentResourceModiferAsync() should not cause a deadlock. (Do not use Semaphore ).

Basically, I think that what I'm looking for is a "stack-based lock" if such a thing exists or is even possible. Because when the asynchronous method continues after waiting, it restores the state of the stack. I was looking a lot for someone else who had this problem, but failed. This probably means that I'm making the situation too complicated, but I'm curious what people can say about it. Maybe something really obvious, I went 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(); } } } 
+1
source share
3 answers

The resource has its own lock and unlock code to handle concurrency. A resource can be changed by any thread.

There is a yellow flag. I believe that the design in which you protect resources (rather than protect them) is usually better in the long run.

When button1 is pressed, its handler performs some modification of r itself, and then asynchronously calls _IndependentResourceModifierAsync (), which performs some modification of r in the given task. _IndependentResourceModifierAsync () gets an r lock before this. In addition, since the handler tinkers with r, it also gets an r-lock.

And there is a red flag. Recursive castles are almost always a bad idea. I explain my arguments on my blog.

There was also another warning that I chose regarding design:

If button1 or button2 is pressed when the resource is locked by the main thread, an exception will be thrown. (It is not possible to use a monitor or Mutex because they are flow controlled)

It doesn’t sound like that. Is there any other way to do this? Disabling the buttons as the state changes seems more enjoyable.


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

So your code will look 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 was looking a lot for someone else who had this problem, but failed. This probably means that I'm making the situation too complicated, but I'm curious what people can say about it.

For a long time it was impossible (in general, a period, a complete stop). With .NET 4.5, this is possible, but not very. It is very difficult. I do not know who actually does this in production, and I certainly do not recommend it.

However, I played 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 agreement of AsyncEx already canceled 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 not very long, but it is terribly thought-provoking. It starts with an implicit asynchronous context , which I describe in detail on my blog (which is difficult to understand on my own), and then uses custom expectations to "enter" the code at the right time in the async end-user methods.

You are right on the verge of someone experimenting. RecursiveAsyncLock is not fully tested and probably never will be.

Drag the conductor carefully. There will be dragons.

+3
source

I believe that an asynchronous reentry lock that behaves reasonably well is not possible. This is because when you start an asynchronous operation, you do not need await immediately.

For example, imagine that you changed the 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 returned asynchronously, the code that works with r in the event handler and the code in the called asynchronous method can work simultaneously (because they can be executed on different threads). This means that such a lock will not be safe.

+4
source

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

  • This is not a repeat participant (it does not belong to the thread)
  • It supports asynchronous wait ( WaitAsync )

I don’t have time to check your script right now, but I think this will work.

EDIT: I just noticed this question:

Because when the asynchronous method continues after waiting, it restores the state of the stack.

No, this is absolutely not the case. This is easy to show - add an async method that responds to the button, like this:

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

Set a breakpoint for both of your Console.WriteLine calls - you will notice that before await you have a stack trace, including the "button control" code in WinForms; after that, the stack will look completely different.

+3
source

Source: https://habr.com/ru/post/1012921/


All Articles