Skip to content

Instantly share code, notes, and snippets.

@binki
Last active December 12, 2019 18:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save binki/033a61e832ac211f1a9df593b0d3aa91 to your computer and use it in GitHub Desktop.
Save binki/033a61e832ac211f1a9df593b0d3aa91 to your computer and use it in GitHub Desktop.
AsyncEx-3.0.1 AsyncConditionVariable deadlock repro
using System;
namespace AsyncExAsyncProducerConsumerQueueDeadlock
{
class FuncUtil
{
public static T Invoke<T>(Func<T> func) => func();
}
}
using Nito.AsyncEx;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncExAsyncProducerConsumerQueueDeadlock
{
class Program
{
static async Task Main(string[] args)
{
var asyncLock = new AsyncLock();
var asyncCondition = new AsyncConditionVariable(asyncLock);
using (var cancelSource = new CancellationTokenSource())
{
var ct = cancelSource.Token;
Task waiter1Task;
// 1. Lock the lock.
using (await asyncLock.LockAsync())
{
Console.WriteLine("Main locked");
// 2. Add a waiter on the lock.
waiter1Task = FuncUtil.Invoke(async () =>
{
Task cancellerTask;
Console.WriteLine("Waiter 1 waiting.");
using (await asyncLock.LockAsync())
{
// Now that we have continued, we should be holding asyncCondition._mutex.
Console.WriteLine($"Waiter 1 locked");
// 4. Cancel on another thread.
cancellerTask = Task.Run(() =>
{
Console.WriteLine($"Canceller cancelling…");
cancelSource.Cancel();
Console.WriteLine($"Canceller cancelled");
});
Console.WriteLine($"Waiter 1 sleeping…");
// 5. Ensure that the Cancel() was called on another thread. It should start waiting
// on asyncCondition._mutex.
Thread.Sleep(1000);
Console.WriteLine($"Waiter 1 woke, notifying…");
// 6. Trigger an attempt to have asyncCondition.WaitAsync(ct) from step 3 unregister.
//
// In AsyncEx-3.0.1, this calls TaskCompletionSource.SetResult(). In AsyncEx-4.0.1,
// it instead calls TrySetResultWithBackgroundContinuations(), avoiding the deadlock.
asyncCondition.Notify();
Console.WriteLine($"Waiter 1 sleeping more…");
Thread.Sleep(1000);
Console.WriteLine($"Waiter 1 woke up");
}
Console.WriteLine($"Waiter 1 unlocked");
await cancellerTask;
});
// 3. Release using condition with cancellation token.
Console.WriteLine($"Main waiting on condition…");
await asyncCondition.WaitAsync(ct);
Console.WriteLine($"Main woke from condition.");
}
await waiter1Task;
}
}
}
}
@binki
Copy link
Author

binki commented Dec 12, 2019

I think that the change that causes this code to run without deadlocking in AsyncEx-4.0.1 is StephenCleary/AsyncEx@e287838#diff-f690463a0e3b52f06273eacfa56636b6 . It unconditionally deadlocks for me in AsyncEx-3.0.1 (assuming the unsynchronized Thread.Sleep() calls get things lined up correctly) even though it feels like it shouldn’t.

This is the root cause of an issue which was causing one of my programs to deadlock while using AsyncProducerConsumerQueue<T>. It looks like I will have to upgrade to AsyncEx-4.0.1.

I would prefer if I could use the AsyncEx-3.0.1 code but just with a TCS made with TaskCreationOptions.RunContinuationsAsynchronously. Hmm, I have probably spent too much time on this.

Sorry to bother you, but just and FYI if you wanted to see an example of a deadlock on your ancient, unsupported AsyncEx-3.0.1 which I’m still using, @StephenCleary

@binki
Copy link
Author

binki commented Dec 12, 2019

AsyncProducerConsumerQueue doesn’t let me override the AsyncWaitQueue implementation used by its AsyncLock, so I can’t work around this by using my alternative TaskCreationOptions.RunContinuationsAsynchronously-based IAsyncWaitQueue<T> implementation. Aww :-p

@StephenCleary
Copy link

@binki Actually, that wouldn't work anyway. In the v3 era, the internals of the synchronization primitives only work correctly if TCS instances are completed synchronously. The v4 changes included a lot of other changes so that asynchronous completion is possible.

@binki
Copy link
Author

binki commented Dec 12, 2019

@StephenCleary Ah, I didn’t realize that. I probably have introduced subtle bugs due to my hacks. I am going to see if upgrading to 5.0.0 is viable.

If you have time: is V3 known/expected to have unavoidable deadlocks? Is my above repro doing something it shouldn’t be?

Thanks so much for your response!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment