Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: reentrancibility of async lock #257

Open
matthew-a-thomas opened this issue Jun 20, 2022 · 4 comments 路 May be fixed by #258
Open

Docs: reentrancibility of async lock #257

matthew-a-thomas opened this issue Jun 20, 2022 · 4 comments 路 May be fixed by #258

Comments

@matthew-a-thomas
Copy link

matthew-a-thomas commented Jun 20, 2022

Your documentation for AsyncLock currently says:

It's only almost equivalent because the lock keyword permits reentrancy, which is not currently possible to do with an async-ready lock.

Technically reentrancy is possible 馃槈

P.S. I'm aware this has been attempted a gazillion times before, including by you. But I'm hopeful to have found the proper proportion of ingredients鈥攃heck out the kinds of tests that pass! I know of no other existing implementation that can pass all of these tests.

@matthew-a-thomas matthew-a-thomas linked a pull request Jun 25, 2022 that will close this issue
@timcassell
Copy link

timcassell commented Nov 29, 2022

I looked into it, and saw that even your own solution fails this simple test. (Credit)

AsyncLock _asyncLock = new AsyncLock(allowReentry: true);
int CurrentThreadId => Thread.CurrentThread.ManagedThreadId;

async Task MainAsync()  
{  
    using (await _asyncLock.LockAsync())  
    {  
        Console.WriteLine($"1. {nameof(MainAsync)} starting on thread {CurrentThreadId}");  
          
        var childTask = ChildAsync(delay: TimeSpan.FromMilliseconds(100));  
        await Task.Delay(TimeSpan.FromMilliseconds(101)).ConfigureAwait(false);  
          
        Console.WriteLine($"4. {nameof(MainAsync)} continuing on thread {CurrentThreadId}");  
        NonThreadSafe();  
        Console.WriteLine($"6. {nameof(MainAsync)} finished {nameof(NonThreadSafe)}");  
  
        await childTask;  
    }  
}  
  
async Task ChildAsync(TimeSpan delay)  
{  
    Console.WriteLine($"2. {nameof(ChildAsync)} starting on thread {CurrentThreadId}");  
  
    await Task.Delay(delay).ConfigureAwait(false);  
      
    using (await _asyncLock.LockAsync())  
    {  
        Console.WriteLine($"3. {nameof(ChildAsync)} continuing on thread {CurrentThreadId}");  
        NonThreadSafe();  
        Console.WriteLine($"5. {nameof(ChildAsync)} finished {nameof(NonThreadSafe)}");  
    }  
}

I do believe it is possible to solve this case, but only with cooperation from Tasks themselves. Which means the AsyncLock would have to be included in the TPL/BCL itself. And the cost of that cooperation would also slow down tasks in the general case, making it highly unlikely to ever be adopted.

But even then, there would be problems with it: such a solution could not support synchronous locks in a safe way. This simple test would deadlock. Also, it would only be safe to use with Tasks, and it would be dangerous to use with any other custom task-like type (including ValueTask, since they can be backed by any custom type).

@matthew-a-thomas
Copy link
Author

matthew-a-thomas commented Nov 30, 2022

@timcassell That article you linked to was actually my main motivation for working on this. Max Fedotov said it's impossible and I wanted to prove him wrong. As I'm sure you noticed, I'm footnote number two in his article. You can read more of our back and forth by following the links on my website. Edit: actually this article (the last in that series) is probably more relevant to this discussion.

The example code you've posted would indeed be unpleasant with my lock. But allow me to explain why I don't think this is significant enough to call my lock "broken".

Notice how that code alters the current synchronization context while in the guarded section of the lock. await Task.Delay(TimeSpan.FromMilliseconds(101)).ConfigureAwait(false); is the line that does that, and it does that with .ConfigureAwait(false). However I explicitly advise people not to do that with my lock. So I don't consider this to be any more threatening to my lock than it's a threat to synchronous locks when you call Monitor.Exit while in a lock body. Synchronous locks are not broken and impossible just because you can abuse them. So also as long as people use my library in the way I intend them to then they will have a working reentrant async lock and the two calls to NonThreadSafe() in your example code will happily be serialized.

That aside, and even without any .ConfigureAwait(false), your code does come close to hitting what I've already identified (with Max's help) here. That's because (if my lock were used in your code) you will have trickled my magic synchronization context down into childTask鈥攜ou'll have reentered my lock鈥攂ut then you run both lock-holding asynchronous branches in parallel. As a result any awaits within the guarded sections while that is going on will effectively toggle between the two branches. And that can be... weird. The issue I linked to perhaps gives a more clear example of this happening. But even with that weirdness I'm not convinced my lock is faulty. Although I haven't put much thought into how to improve those ergonomics I suspect an elegant solution is possible. And if not then I'll just have to add another warning to the label.

And at the end of the day I think it's significant that my lock does indeed do things that no other lock can do. Furthermore I think that if you stand far enough back and squint at Max's article, those are exactly the things that he says are impossible.

@timcassell
Copy link

@matthew-a-thomas Thanks, I did miss some of that conversation. Also, I started a discussion in my repo about an idea, would love to get your thoughts on it.

@matthew-a-thomas
Copy link
Author

I just noticed that Cogs.Threading.ReentrantAsyncLock is another correct implementation: https://dotnetfiddle.net/6WdVy1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants