Skip to content

Instantly share code, notes, and snippets.

@ChrisMcKee
Last active December 6, 2023 11:37
Show Gist options
  • Save ChrisMcKee/6664438 to your computer and use it in GitHub Desktop.
Save ChrisMcKee/6664438 to your computer and use it in GitHub Desktop.
AsyncHelpers to simplify calling of Async/Task methods from synchronous context. (use https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs this is ancient code)
namespace My.Common
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public static class AsyncHelpers
{
/// <summary>
/// Execute's an async Task<T> method which has a void return value synchronously
/// </summary>
/// <param name="task">
/// Task<T> method to execute
/// </param>
public static void RunSync(Func<Task> task)
{
var oldContext = SynchronizationContext.Current;
var synch = new ExclusiveSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(synch);
synch.Post(async _ =>
{
try
{
await task();
}
catch (Exception e)
{
synch.InnerException = e;
throw;
}
finally
{
synch.EndMessageLoop();
}
}, null);
synch.BeginMessageLoop();
SynchronizationContext.SetSynchronizationContext(oldContext);
}
/// <summary>
/// Execute's an async Task<T> method which has a T return type synchronously
/// </summary>
/// <typeparam name="T">Return Type</typeparam>
/// <param name="task">
/// Task<T> method to execute
/// </param>
/// <returns></returns>
public static T RunSync<T>(Func<Task<T>> task)
{
var oldContext = SynchronizationContext.Current;
var synch = new ExclusiveSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(synch);
T ret = default(T);
synch.Post(async _ =>
{
try
{
ret = await task();
}
catch (Exception e)
{
synch.InnerException = e;
throw;
}
finally
{
synch.EndMessageLoop();
}
}, null);
synch.BeginMessageLoop();
SynchronizationContext.SetSynchronizationContext(oldContext);
return ret;
}
private class ExclusiveSynchronizationContext : SynchronizationContext
{
private readonly Queue<Tuple<SendOrPostCallback, object>> _items =
new Queue<Tuple<SendOrPostCallback, object>>();
private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
private bool done;
public Exception InnerException { get; set; }
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("We cannot send to our same thread");
}
public override void Post(SendOrPostCallback d, object state)
{
lock (_items)
{
_items.Enqueue(Tuple.Create(d, state));
}
_workItemsWaiting.Set();
}
public void EndMessageLoop()
{
Post(_ => done = true, null);
}
public void BeginMessageLoop()
{
while (!done)
{
Tuple<SendOrPostCallback, object> task = null;
lock (_items)
{
if (_items.Count > 0)
{
task = _items.Dequeue();
}
}
if (task != null)
{
task.Item1(task.Item2);
if (InnerException != null) // the method threw an exeption
{
throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
}
}
else
{
_workItemsWaiting.WaitOne();
}
}
}
public override SynchronizationContext CreateCopy()
{
return this;
}
}
}
}
@DASHlastar
Copy link

Fixed documentation of public methods and renamed class

- public static class AsyncHelpers

  • public static class AsyncHelper

- /// Execute's an async Task method which has a void return value synchronously

  • /// Execute's an async Task method which has a void return value synchronously

- /// Task method to execute

  • /// Task method to execute

- /// Execute's an async Task method which has a T return type synchronously

  • /// Execute's an async Task<T> method which has a T return type synchronously

- /// Task method to execute

  • /// Task<T> method to execute

@gabpaiz3
Copy link

I had a question on lines 79 and 80, had you considered using a ConcurrentQueue? I believe you could eliminate the lock statements that follow if it was used instead of Queue.

@markwhitfeld
Copy link

Thanks! Awesome helper class.
I just noticed that the original SynchronisationContext will not be restored if there is an exception. Is this intentional?

@shahab1khan
Copy link

Under high load this code may cause deadlock because of eventual non availability of ThreadPool threads.

Here is my analysis:

  1. RunAsync method will queue a delegate on the ExclusiveSynchronizationContext's queue using Post method.
  2. The BeginMessageLoop will fetch the delegate from the queue and execute it. After calling the delegate, BeginMessageLoop will end up waiting on _workItemsWaiting event.
  3. The delegate inturn awaits the task. This task will get dispatched if a ThreadPool thread is available.
  4. Let’s assume that the task mentioned above takes some time. In an application where RunAsync is called multiple times from multiple tasks (e.g. invoking an async function to process messages read from a queue), the calling task-threads will never be returned to the threadpool because of encountering call to _workItemsWaiting.WaitOne.
    When number of such blocked Task-Threads reaches the max threadpool threads, the task awaited in the original delegate passed to Post method (line 25) will never be dispatched/ run. This will result in synch.EndMessageLoop(); in finally block to never execute. This means _workItemsWaiting will not be signalled. This means that the blocked Task-threads will remain blocked forever.
    I think the main pitfall here is that we are mixing two different asynchronous modes of execution. To make sure that the above problem doesn’t happen one will need to be aware of MaxThreadCount of the threadpool and set an upper ceiling on the parallel execution of RunSync method. This will ensure that there are enough threads available on the threadpool and Task Scheduler is not starved of threads. One need to be careful here though because of RunAsync is called from multiple places in the call hierarchy, setting the ceiling at the uppermost level may be misleading.

@ssteiner
Copy link

My colleague was using this in his ASP.NET application.. and the async methods he ran threw errors every now and then that would lead in spinners spinning forever because responses came back not in the AspNetSynchronizationContext, but the ExclusiveSynchronizationContext.

The issue is with the Aggregate exception that is thrown in BeginMessageLoop.. it is returned during execution of
synch.BeginMessageLoop()
thus resulting in
SynchronizationContext.SetSynchronizationContext(oldContext);
never running. If you wrap the entire block from
synch.Post(..
into a try, and put the context restoration in the finally, the issue goes away.

@stevenxi
Copy link

We've recently discovered a very strange behavior from this helper. It kind of "leaks" the ExclusiveSynchronizationContext.
First, we've slightly modified the code from what it shows in the OP. We put the SynchronizationContext.SetSynchronizationContext(oldContext) in a finally {} block. So it's always been called.

The strange behavior seems happen like this:

  1. In one thread, completely independent context (System.Timers.Timer.MyTimerCallback). Original SynchronizationContext.Current is null. We called AsyncHelpers.RunSync() to invoke an 3rd party async API (MongoDb).

  2. Another thread, completely independent context (an independent dispatching thread). Original SynchronizationContext.Current is null, we called that 3rd party async API with an await. After the call, SynchronizationContext.Current becomes ExclusiveSynchronizationContext. And with or without .ConfigureAwait(false), results same.

It happens occasionally.
I've checked source code of MongoDb's c# client, everything seems normal.

So I'm kind of suspect the ExclusiveSynchronizationContext may not work as expected, but couldn't figure out how.
Again, I wouldn't say it's this helper's issue for sure. It could be MongoDb's c# client's problem. But I don't know under what kind of scenario this behavior could happen.

@kkorus
Copy link

kkorus commented Apr 14, 2018

How does .RunSync<T> differs from just using .Result ?

@martinlingstuyl
Copy link

into a try, and put the context restoration in the finally, the issue

Do you have an example of how that would work @ssteiner? I'm also experiencing deadlocks during AsyncHelper execution.

@GFoley83
Copy link

For those coming from Google, just use this helper class from the official Microsoft Identity repo to call async methods synchronously:

https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

// Copyright (c) Microsoft Corporation, Inc. All rights reserved.
// Licensed under the MIT License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.AspNet.Identity
{
    internal static class AsyncHelper
    {
        private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None,
            TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

        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();
        }
    }
}

@ogix
Copy link

ogix commented Sep 29, 2020

Thanks, @GFoley83!

@ChrisMcKee
Copy link
Author

For those coming from Google, just use this helper class from the official Microsoft Identity repo to call async methods synchronously:

https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

// Copyright (c) Microsoft Corporation, Inc. All rights reserved.
// Licensed under the MIT License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.AspNet.Identity
{
    internal static class AsyncHelper
    {
        private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None,
            TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

        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();
        }
    }
}

Added that ref to the top of this; also this is the first ever notification I've had from a gist... nice of github to finally add that as I hadn't seen a single message here 😆

@HaraldMuehlhoffCC
Copy link

HaraldMuehlhoffCC commented Dec 22, 2020

While the internal asp.net solution from Microsoft may work fine on the asp.net core side of things I'm getting deadlocks when using it in a WPF client while the code given here is running without problems! So be sure to test it; the code is not equivalent! @ChrisMcKee

PS: Using the ExclusiveSynchronizationContext version given here for the client and the "Microsoft version" for the asp.net core server side seems to work; BUT the seemingly simpler Microsoft version is running slower over 20% slower in my test case on my machine.

@molekp
Copy link

molekp commented Feb 24, 2022

You should also notice that Microsoft version will not throw exception from executed task. It just will run it and wait for end. Your version will rethrow the exception from the task.

You should consider it when choosing version of AsyncHelper.

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