43

I'm in the process of updating a library that has an API surface that was built in .NET 3.5. As a result, all methods are synchronous. I can't change the API (i.e., convert return values to Task) because that would require that all callers change. So I'm left with how to best call async methods in a synchronous way. This is in the context of ASP.NET 4, ASP.NET Core, and .NET/.NET Core console applications.

I may not have been clear enough - the situation is that I have existing code that is not async aware, and I want to use new libraries such as System.Net.Http and the AWS SDK that support only async methods. So I need to bridge the gap, and be able to have code that can be called synchronously but then can call async methods elsewhere.

I've done a lot of reading, and there are a number of times this has been asked and answered.

Calling async method from non async method

Synchronously waiting for an async operation, and why does Wait() freeze the program here

Calling an async method from a synchronous method

How would I run an async Task<T> method synchronously?

Calling async method synchronously

How to call asynchronous method from synchronous method in C#?

The problem is that most of the answers are different! The most common approach I've seen is use .Result, but this can deadlock. I've tried all the following, and they work, but I'm not sure which is the best approach to avoid deadlocks, have good performance, and plays nicely with the runtime (in terms of honoring task schedulers, task creation options, etc). Is there a definitive answer? What is the best approach?

private static T taskSyncRunner<T>(Func<Task<T>> task)
    {
        T result;
        // approach 1
        result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 2
        result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 3
        result = task().ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 4
        result = Task.Run(task).Result;

        // approach 5
        result = Task.Run(task).GetAwaiter().GetResult();


        // approach 6
        var t = task();
        t.RunSynchronously();
        result = t.Result;

        // approach 7
        var t1 = task();
        Task.WaitAll(t1);
        result = t1.Result;

        // approach 8?

        return result;
    }


  • The answer is you don't do it. You add new methods that are asyncronous and keep the old synchronous ones there for the legacy callers. - Scott Chamberlain
  • That seems a little harsh, and really kills the ability to use new code. For example, the new version of the AWS SDK doesn't have non-async methods. Same for a number of other third party libraries. So unless you rewrite the world, you can't use any of these? - Erick T
  • Option 8: Maybe the TaskCompletionSource could be an option ? - OrdinaryOrange
  • Result property only causes deadlocks when called while Task is still running. I'm not 100% sure but you should be safe if you properly wait until the task ends, as in approach 7. - infiniteRefactor
  • Showing sample of why you can't use synchronous API from asynchronous code could help finding better answer than on by @scott - Alexei Levenkov

2 답변


58

So I'm left with how to best call async methods in a synchronous way.

First, this is an OK thing to do. I'm stating this because it is common on Stack Overflow to point this out as a deed of the devil as a blanket statement without regard for the concrete case.

It is not required to be async all the way for correctness. Blocking on something async to make it sync has a performance cost that might matter or might be totally irrelevant. It depends on the concrete case.

Deadlocks come from two threads trying to enter the same single-threaded synchronization context at the same time. Any technique that avoids this reliably avoids deadlocks caused by blocking.

Here, all your calls to .ConfigureAwait(false) are pointless because you are not awaiting.

RunSynchronously is invalid to use because not all tasks can be processed that way.

.GetAwaiter().GetResult() is different from Result/Wait() in that it mimics the await exception propagation behavior. You need to decide if you want that or not. (So research what that behavior is; no need to repeat it here.)

Besides that, all these approaches have similar performance. They will allocate an OS event one way or another and block on it. That's the expensive part. I don't know which approach is absolutely cheapest.

I personally like the Task.Run(() => DoSomethingAsync()).Wait(); pattern because it avoids deadlocks categorically, is simple and does not hide some exceptions that GetResult() might hide. But you can use GetResult() as well with this.



20

I'm in the process of updating a library that has an API surface that was built in .NET 3.5. As a result, all methods are synchronous. I can't change the API (i.e., convert return values to Task) because that would require that all callers change. So I'm left with how to best call async methods in a synchronous way.

There is no universal "best" way to perform the sync-over-async anti-pattern. Only a variety of hacks that each have their own drawbacks.

What I recommend is that you keep the old synchronous APIs and then introduce asynchronous APIs alongside them. You can do this using the "boolean argument hack" as described in my MSDN article on Brownfield Async.

First, a brief explanation of the problems with each approach in your example:

  1. ConfigureAwait only makes sense when there is an await; otherwise, it does nothing.
  2. Result will wrap exceptions in an AggregateException; if you must block, use GetAwaiter().GetResult() instead.
  3. Task.Run will execute its code on a thread pool thread (obviously). This is fine only if the code can run on a thread pool thread.
  4. RunSynchronously is an advanced API used in extremely rare situations when doing dynamic task-based parallelism. You're not in that scenario at all.
  5. Task.WaitAll with a single task is the same as just Wait().
  6. async () => await x is just a less-efficient way of saying () => x.
  7. Blocking on a task started from the current thread can cause deadlocks.

Here's the breakdown:

// Problems (1), (3), (6)
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (3)
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (7)
result = task().ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (2), (3)
result = Task.Run(task).Result;

// Problems (3)
result = Task.Run(task).GetAwaiter().GetResult();

// Problems (2), (4)
var t = task();
t.RunSynchronously();
result = t.Result;

// Problems (2), (5)
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;

Instead of any of these approaches, since you have existing, working synchronous code, you should use it alongside the newer naturally-asynchronous code. For example, if your existing code used WebClient:

public string Get()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

and you want to add an async API, then I would do it like this:

private async Task<string> GetCoreAsync(bool sync)
{
  using (var client = new WebClient())
  {
    return sync ?
        client.DownloadString(...) :
        await client.DownloadStringTaskAsync(...);
  }
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

or, if you must use HttpClient for some reason:

private string GetCoreSync()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

private static HttpClient HttpClient { get; } = ...;

private async Task<string> GetCoreAsync(bool sync)
{
  return sync ?
      GetCoreSync() :
      await HttpClient.GetString(...);
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

With this approach, your logic would go into the Core methods, which may be run synchronously or asynchronously (as determined by the sync parameter). If sync is true, then the core methods must return an already-completed task. For implemenation, use synchronous APIs to run synchronously, and use asynchronous APIs to run asynchronously.

Eventually, I recommend deprecating the synchronous APIs.


  • Can you explain more the item six? - Emerson Soares
  • @EmersonSoares: As I explain in my async intro, you can await the result of a method because it returns Task, not because it's async. This means that for trivial methods, you can drop the keywords. - Stephen Cleary
  • I faced recently with same problem in asp.net mvc 5 + EF6. I used TaskFactory as this answer suggests stackoverflow.com/a/25097498/1683040, and it did the trick for me :), but not sure for other scenarios. - LeonardoX
  • I understand your proposal using WebClient. But what benefit does the Core method has when using HttpClient? Wouldn't it be cleaner and more efficient if the synchronous method directly calls the the GetCoreSync method, leaving the boolean argument hack out in this case? - Creepin
  • @Creepin: Yes, I believe so. - Stephen Cleary

Linked


Related

Latest