wooohoh wooohoh - 1 month ago 24
C# Question

Simulate async deadlock in a console application

Usually, an async deadlock occurs in UI thread or with ASP.NET context. I'm trying to simulate the deadlock in a console application so that I can unit test my library codes.

So here's my attempt:

class Program
{
private static async Task DelayAsync()
{
Console.WriteLine( "DelayAsync.Start" );
await Task.Delay( 1000 );
Console.WriteLine( "DelayAsync.End" );
}

// This method causes a deadlock when called in a GUI or ASP.NET context.
public static void Deadlock()
{
Console.WriteLine( "Deadlock.Start" );
// Start the delay.
var delayTask = DelayAsync();
// Wait for the delay to complete.
delayTask.Wait();
Console.WriteLine( "Deadlock.End" );
}

static void Main( string[] args )
{
var thread = new Thread( () =>
{
Console.WriteLine( "Thread.Start" );
SynchronizationContext.SetSynchronizationContext( new DedicatedThreadSynchronisationContext() );
Deadlock();
Console.WriteLine( "Thread.End" );
} );
thread.Start();
Console.WriteLine( "Thread.Join.Start" );
thread.Join();
Console.WriteLine( "Thread.Join.End" );
Console.WriteLine( "Press any key to exit" );
Console.ReadKey( true );
Console.WriteLine( "Pressed" );
}
}


So Deadlock() should cause a deadlock in an right context.
To simulate the ASP.NET context, I'm using DedicatedThreadSynchronisationContext from http://stackoverflow.com/a/31714115/121240:

public sealed class DedicatedThreadSynchronisationContext : SynchronizationContext, IDisposable
{
public DedicatedThreadSynchronisationContext()
{
m_thread = new Thread( ThreadWorkerDelegate );
m_thread.Start( this );
}

public void Dispose()
{
m_queue.CompleteAdding();
}

/// <summary>Dispatches an asynchronous message to the synchronization context.</summary>
/// <param name="d">The System.Threading.SendOrPostCallback delegate to call.</param>
/// <param name="state">The object passed to the delegate.</param>
public override void Post( SendOrPostCallback d, object state )
{
if ( d == null ) throw new ArgumentNullException( "d" );
m_queue.Add( new KeyValuePair<SendOrPostCallback, object>( d, state ) );
}

/// <summary> As
public override void Send( SendOrPostCallback d, object state )
{
using ( var handledEvent = new ManualResetEvent( false ) )
{
Post( SendOrPostCallback_BlockingWrapper, Tuple.Create( d, state, handledEvent ) );
handledEvent.WaitOne();
}
}

public int WorkerThreadId { get { return m_thread.ManagedThreadId; } }
//=========================================================================================

private static void SendOrPostCallback_BlockingWrapper( object state )
{
var innerCallback = ( state as Tuple<SendOrPostCallback, object, ManualResetEvent> );
try
{
innerCallback.Item1( innerCallback.Item2 );
}
finally
{
innerCallback.Item3.Set();
}
}

/// <summary>The queue of work items.</summary>
private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> m_queue =
new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();

private readonly Thread m_thread = null;

/// <summary>Runs an loop to process all queued work items.</summary>
private void ThreadWorkerDelegate( object obj )
{
SynchronizationContext.SetSynchronizationContext( obj as SynchronizationContext );

try
{
foreach ( var workItem in m_queue.GetConsumingEnumerable() )
workItem.Key( workItem.Value );
}
catch ( ObjectDisposedException ) { }
}
}


I set the context before calling Deadlock():

SynchronizationContext.SetSynchronizationContext( new DedicatedThreadSynchronisationContext() );


I expect the code to hang on this line because it should capture the context:

await Task.Delay( 1000 );


However , it passes just fine and the program runs through the end, and it prints "Pressed". (Although the program hangs on DedicatedThreadSynchronisationContext.ThreadWorkerDelegate() so it doesn't exist, but I consider it a minor issue.)

Why doesn't it produce a dead lock? What is the proper way to simulate a dead lock?

Answer

Because Deadlock doesn't run on the same thread as your synchronization context.

You need to make sure to run Deadlock on the synchronization context - just setting the context and calling a method doesn't ensure that.

The easiest way to do this with little modification is to send the Deadlock synchronously to the synchronization context:

SynchronizationContext.Current.Send(_ => Deadlock(), null);

This gives you a nice deadlock on the delay task wait :)