3
\$\begingroup\$

Sometimes I have the need to call an async function from a non-async function and get the result back synchronously. Therefore I wrote the following helper function to be able to do this in one line.

public static T RunSync<T>(Func<Task<T>> taskConstructor)
{
    var signal = new ManualResetEventSlim();
    T result;
    Exception ex = null;
    ThreadPool.QueueUserWorkItem(async () =>
    {
        try
        {
            var task = taskConstructor();
            result = await task.ConfigureAwait(false);
        }
        catch (Exception iex)
        {
            ex = iex;
        }
        finally
        {
            signal.Set();
        }

    });
    signal.Wait();
    if (ex != null)
        throw ex;
    return result;
}

Is this ok to do or will it behave unexpectedly in some edge cases? I know that writing this kind of async functions in can be tricky and can easily deadlock. That's why I ran it inside a ThreadPool thread which should prevent any kind of deadlock.

I think that this is probably not the most performant way to do it, but preventing deadlocks is more important in my case.

I know that re-throwing an exception is not ideal, but I think this is a compromise I have to make here.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Does it have any benefit over just doing task.ConfigureAwait(false) .GetAwaiter() .GetResult(); \$\endgroup\$
    – Anders
    Apr 25 '20 at 9:17
  • \$\begingroup\$ @Anders Good question actually. I think it does. I think your version may deadlock if the task does not use ´ConfigureAwait(false)´ internally. \$\endgroup\$
    – LittleEwok
    Apr 26 '20 at 19:45
  • 1
    \$\begingroup\$ This article shows why code such at this is a bad idea in general. \$\endgroup\$
    – Alejandro
    Apr 27 '20 at 15:07
3
\$\begingroup\$
public static class TaskExtensions
{
    private static readonly TaskFactory TaskFactory = new TaskFactory(
            CancellationToken.None, 
            TaskCreationOptions.None, 
            TaskContinuationOptions.None, 
            TaskScheduler.Default);

    public static TResult RunSync<TResult>(this Func<Task<TResult>> asyncFunc)
        => TaskFactory
            .StartNew(asyncFunc)
            .Unwrap()
            .GetAwaiter()
            .GetResult();

    public static void RunSync(this Func<Task> asyncAction)
        => TaskFactory
            .StartNew(asyncAction)
            .Unwrap()
            .GetAwaiter()
            .GetResult();
}

There are several things that worth mentioning:

  • Async methods were designed to be used all the way. So, if you call an async I/O operation in the bottom layer then it should be be called in an async fashion till the top layer.
  • Async operations are sensitive for Exceptions. They can behave differently based on how you call them. (They can be swallowed, thrown as an AggregateException or thrown normally. Here the UnWrap + GetAwaiter do the magic for us to be able to handle the exception normally.
  • In order to avoid deadlocks the async operation is passed to an other Task, that is where TaskFactory.StartNew comes into play.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.