AsyncHelper Can Be Considered Harmful
.NET developers have been in the transition to move from synchronous APIs to asynchronous APIs. That was boosted a lot by the await
/async
keywords introduced in C# 5.0. However, we are now in a dangerous middle ground: there are as many synchronous APIs as there are async ones.
The mix of them requires the ability to call async APIs from a synchronous context, and vice versa. Calling synchronous APIs from an async context is simple—you can fire up a task and let it do the work. Calling async APIs from a sync context is much more complicated, and that is where AsyncHelper
comes to the play.
What is AsyncHelper?
AsyncHelper
is a common class used to run async code in a synchronous context. It’s a simple helper class with two methods to run async APIs.
Here is a typical implementation of the core logic:
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
var cultureUi = CultureInfo.CurrentUICulture;
var culture = CultureInfo.CurrentCulture;
return _myTaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return func();
}).Unwrap().GetAwaiter().GetResult();
}
public static void RunSync(Func<Task> func)
{
var cultureUi = CultureInfo.CurrentUICulture;
var culture = CultureInfo.CurrentCulture;
_myTaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return func();
}).Unwrap().GetAwaiter().GetResult();
}
There are slight variants, with and without setting the CurrentCulture
and CurrentUICulture
, but the main part is still:
- Spawning a new
Task
to run the async task. - Blocking and getting the result using
.Unwrap().GetAwaiter().GetResult()
.
The Misconception and the Danger
One of the reasons this helper became popular was the misconception that it was written by Microsoft as a general recommendation and must therefore be safe. This is not true. The class was introduced as an internal class within AspNetIdentity (see GitHub source). This means Microsoft teams can use it when they think it’s the right choice, but it is not the default recommendation for running async tasks in a synchronous context.
Unfortunately, I’ve seen a fair share of threads stuck in the AsyncHelper.RunSync
stack trace, likely having fallen victim to a deadlock situation.
A typical stuck stack trace might look something like this:
756A477F9790 75ABD117CF16 [HelperMethodFrame_1OBJ] (System.Threading.Monitor.ObjWait)
756A477F98C0 75AB62F11BF9 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)
756A477F9970 75AB671E0529 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken)
756A477F99D0 75AB671E0060 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken)
756A477F9A40 75AB676068B8 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions)
756A477F9A60 75AB661E4FE7 System.Runtime.CompilerServices.TaskAwaiter`1[[System.__Canon, System.Private.CoreLib]].GetResult()
An excellent further explanation of why this blocking pattern is problematic can be read in this Stack Overflow thread on Task.Result
vs .GetAwaiter.GetResult()
.
Conclusion
Async/sync is a complex topic, and even experienced developers make mistakes. There is no simple, guaranteed-safe way to just run async code in a synchronous context. AsyncHelper
is absolutely not that simple solution.
It is a simple, convenient way to introduce a risk of deadlocks into your application. I see it as a shortcut to solve an immediate problem, but one that creates bigger ones down the path.
Just because you can, doesn’t mean you should. That applies to AsyncHelper perfectly.