Skip to content

Instantly share code, notes, and snippets.

@binki
Last active December 12, 2019 18:55
Show Gist options
  • 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;
}
}
}
}
@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