从非异步代码调用异步方法

我正在更新一个内置了 API 外观的库。NET 3.5.因此,所有方法都是同步的。我不能更改 API (例如,将返回值转换为 Task) ,因为这将要求所有调用方都更改。因此,我留下了如何以同步方式最好地调用异步方法的问题。这是在 ASP.NET 4、 ASP.NET Core 和。NET/.NET 核心控制台应用程序。

我可能还不够清楚——现在的情况是,我有不能异步感知的现有代码,我想使用新的库,比如 System。网。Http 和仅支持异步方法的 AWS SDK。因此,我需要弥合这一差距,并能够拥有可以同步调用但随后可以在其他地方调用异步方法的代码。

我做了很多阅读,有很多次,这已经被问到和回答。

从非异步方法调用异步方法

同步地等待一个异步操作,为什么在这里等待()冻结程序

从同步方法调用异步方法

如何同步运行一个异步 Task < T > 方法?

同步调用异步方法

在 C # 中如何从同步方法调用异步方法?

问题是大多数的答案是不同的!我见过的最常见的方法是使用。结果,但这可能会陷入僵局。我尝试了以下所有方法,它们都有效,但我不确定哪种方法是避免死锁、具有良好性能以及与运行时良好配合的最佳方法(在尊重任务调度程序、任务创建选项等方面)。有明确的答案吗?最好的方法是什么?

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;
}
80564 次浏览

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.

In your code snippet, all calls to .ConfigureAwait(false) are pointless because the return value is not awaited. ConfigureAwait returns a struct that, when awaited, exhibits the behavior that you requested. If that struct is simply dropped, it does nothing.

RunSynchronously is invalid to use because not all tasks can be processed that way. This method is meant for CPU-based tasks, and it can fail to work under certain circumstances.

.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.) If your task contains a single exception then the await error behavior is usually convenient and has little downside. If there are multiple exceptions, for example from a failed Parallel loop where multiple tasks failed, then await will drop all exceptions but the first one. That makes debugging harder.

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. The other machinery is rather cheap compared to that. I don't know which approach is absolutely cheapest.

In case an exception is being thrown, that is going to be the most expensive part. On .NET 5, exceptions are processed at a rate of at most 200,000 per second on a fast CPU. Deep stacks are slower, and the task machinery tends to rethrow exceptions multiplying their cost. There are ways of blocking on a task without the exception being rethrown, for example task.ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously).Wait();.

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

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.

You Can Call Async Method From non-async method .Check below Code .

     public ActionResult Test()
{
TestClass result = Task.Run(async () => await GetNumbers()).GetAwaiter().GetResult();
return PartialView(result);
}


public async Task<TestClass> GetNumbers()
{
TestClass obj = new TestClass();
HttpResponseMessage response = await APICallHelper.GetData(Functions.API_Call_Url.GetCommonNumbers);
if (response.IsSuccessStatusCode)
{
var result = response.Content.ReadAsStringAsync().Result;
obj = JsonConvert.DeserializeObject<TestClass>(result);
}
return obj;
}

I just went thru this very thing with the AWS S3 SDK. Used to be sync, and I built a bunch of code on that, but now it's async. And that's fine: they changed it, nothing to be gained by moaning about it, move on.
So I need to update my app, and my options are to either refactor a large part of my app to be async, or to "hack" the S3 async API to behave like sync.
I'll eventually get around to the larger async refactoring - there are many benefits - but for today I have bigger fish to fry so I chose to fake the sync.

Original sync code was
ListObjectsResponse response = api.ListObjects(request);
and a really simple async equivalent that works for me is
Task<ListObjectsV2Response> task = api.ListObjectsV2Async(rq2);
ListObjectsV2Response rsp2 = task.GetAwaiter().GetResult();

While I get it that purists might pillory me for this, the reality is that this is just one of many pressing issues and I have finite time so I need to make tradeoffs. Perfect? No. Works? Yes.