I have a public async void Foo()
method that I want to call from synchronous method. So far all I have seen from MSDN documentation is calling async methods via
There is, however, a good solution that works in (almost: see comments) every situation: an ad-hoc message pump (SynchronizationContext).
The calling thread will be blocked as expected, while still ensuring that all continuations called from the async function don't deadlock as they'll be marshaled to the ad-hoc SynchronizationContext (message pump) running on the calling thread.
The code of the ad-hoc message pump helper:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Threading
{
/// Provides a pump that supports running asynchronous methods on the current thread.
public static class AsyncPump
{
/// Runs the specified asynchronous method.
/// The asynchronous method to execute.
public static void Run(Action asyncMethod)
{
if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext(true);
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function
syncCtx.OperationStarted();
asyncMethod();
syncCtx.OperationCompleted();
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// Runs the specified asynchronous method.
/// The asynchronous method to execute.
public static void Run(Func asyncMethod)
{
if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext(false);
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completes
var t = asyncMethod();
if (t == null) throw new InvalidOperationException("No task provided.");
t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
t.GetAwaiter().GetResult();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// Runs the specified asynchronous method.
/// The asynchronous method to execute.
public static T Run(Func> asyncMethod)
{
if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext(false);
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completes
var t = asyncMethod();
if (t == null) throw new InvalidOperationException("No task provided.");
t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
return t.GetAwaiter().GetResult();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// Provides a SynchronizationContext that's single-threaded.
private sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
/// The queue of work items.
private readonly BlockingCollection> m_queue =
new BlockingCollection>();
/// The processing thread.
private readonly Thread m_thread = Thread.CurrentThread;
/// The number of outstanding operations.
private int m_operationCount = 0;
/// Whether to track operations m_operationCount.
private readonly bool m_trackOperations;
/// Initializes the context.
/// Whether to track operation count.
internal SingleThreadSynchronizationContext(bool trackOperations)
{
m_trackOperations = trackOperations;
}
/// Dispatches an asynchronous message to the synchronization context.
/// The System.Threading.SendOrPostCallback delegate to call.
/// The object passed to the delegate.
public override void Post(SendOrPostCallback d, object state)
{
if (d == null) throw new ArgumentNullException("d");
m_queue.Add(new KeyValuePair(d, state));
}
/// Not supported.
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("Synchronously sending is not supported.");
}
/// Runs an loop to process all queued work items.
public void RunOnCurrentThread()
{
foreach (var workItem in m_queue.GetConsumingEnumerable())
workItem.Key(workItem.Value);
}
/// Notifies the context that no more work will arrive.
public void Complete() { m_queue.CompleteAdding(); }
/// Invoked when an async operation is started.
public override void OperationStarted()
{
if (m_trackOperations)
Interlocked.Increment(ref m_operationCount);
}
/// Invoked when an async operation is completed.
public override void OperationCompleted()
{
if (m_trackOperations &&
Interlocked.Decrement(ref m_operationCount) == 0)
Complete();
}
}
}
}
Usage:
AsyncPump.Run(() => FooAsync(...));
More detailed description of the async pump is available here.