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

100
我正在更新一个库,它的API表面是在.NET 3.5中构建的。因此,所有方法都是同步的。我不能更改API(即将返回值转换为Task),因为那将需要所有调用者进行更改。所以我只能想办法以最佳方式以同步方式调用异步方法。这是在ASP.NET 4、ASP.NET Core和.NET/.NET Core控制台应用程序的上下文中。
也许我没有说清楚——情况是我有现有的代码,它不支持异步操作,我想使用支持仅异步方法的新库,例如System.Net.Http和AWS SDK。因此,我需要弥合差距,并且能够编写可以被同步调用的代码,但然后可以在其他地方调用异步方法。
我已经阅读了很多资料,有很多次这个问题被问到并得到解答。 从非异步方法调用异步方法 同步等待异步操作,以及为什么Wait()会冻结程序

从同步方法调用异步方法

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

同步地调用异步方法

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

问题在于大多数答案都不同!我见过最常见的方法是使用.Result,但这可能会导致死锁。我尝试了以下所有方法,它们都可以工作,但我不确定哪种方法是避免死锁、具有良好性能并与运行时(在任务调度程序、任务创建选项等方面)协调得好的最佳方法。有明确的答案吗?什么是最佳途径?

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

5
答案是不要这样做。你需要添加新的异步方法,并且保留旧的同步方法供遗留调用者使用。 - Scott Chamberlain
22
这似乎有点过于苛刻,而且会削弱使用新代码的能力。例如,AWS SDK的新版本没有非异步方法,其他一些第三方库也是如此。所以,除非你重写整个世界,否则就不能使用这些库? - Erick T
选项8:也许TaskCompletionSource可以是一个选择? - OrdinaryOrange
1
只有在任务仍在运行时调用Result属性才会导致死锁。虽然我不是100%确定,但如果您正确等待任务结束,就像第7种方法一样,您应该是安全的。 - infiniteRefactor
1
展示为什么不能在异步代码中使用同步API的示例,可能会比@scott提供的答案更好。 - Alexei Levenkov
显示剩余3条评论
4个回答

118

那么我现在面临的问题是如何以同步方式调用异步方法。

首先,这是可以做到的。我提出这个观点是因为在Stack Overflow上,有时会把这种方式一概而论地指责为魔鬼的行为,而不考虑具体情况。

为了正确性,并不需要将所有操作都设为异步。将异步操作阻塞以使其同步化会带来性能成本,这可能很重要,也可能完全无关紧要,这取决于具体情况。

死锁来自于两个线程在同一单线程同步上下文中尝试同时进入。任何可靠避免此类情况的技术都可以避免由阻塞引起的死锁。

在您的代码片段中,所有对.ConfigureAwait(false)的调用都是无意义的,因为未等待返回值。 ConfigureAwait返回一个结构体,在等待时表现出您所请求的行为。如果该结构体被简单地丢弃,则它什么也不会做。

使用RunSynchronously是无效的,因为并非所有任务都可以以这种方式处理。该方法适用于基于CPU的任务,并且在某些情况下可能无法正常工作。

.GetAwaiter().GetResult()Result/Wait()不同,因为它模拟了await异常传播行为。您需要决定是否需要该行为(因此,请研究一下该行为是什么,无需在此重复)。如果您的任务包含单个异常,则使用await错误行为通常很方便,并且缺点很少。如果有多个异常,例如来自于失败的Parallel循环的多个任务,则await将放弃除第一个之外的所有异常。这会使调试变得更加困难。

所有这些方法具有类似的性能。它们将通过某种方式分配操作系统事件并在其上阻塞。这是昂贵的部分。与此相比,其他机制相对较便宜。我不知道哪种方法是绝对最便宜的。

如果引发了异常,那将是成本最高的部分。在.NET 5上,快速CPU处理异常的速度最多为每秒200,000次。深堆栈较慢,任务机制倾向于重新抛出异常,从而增加其成本。有一些方法可以阻止异常被重新抛出而不会阻塞任务,例如task.ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously).Wait();

我个人喜欢Task.Run(() => DoSomethingAsync()).Wait();模式,因为它能够绝对避免死锁,简单易懂且不会隐藏一些可能由GetResult()隐藏的异常。但也可以与GetResult()一起使用。


4
谢谢,这讲得非常清楚易懂。 - Erick T
你也可以这样写Task.Run(async () => await DoSomethingAsync()),但是既然只有一行代码,那真的有必要吗?不管怎样,在DoSomethingAsync()内部的代码如果需要的话仍然可以使用await,对吧? - void.pointer
1
@void.pointer 从语义上讲,它们是等价的,除了 await 只会保留第一个异常。否则,您可以使用喜欢的风格。DoSomethingAsync 在内部做什么是对外部屏蔽的。它可以使用任何机制来产生 Taskawait 会这样做,但 Task.FromResult 和其他方法也会这样做。 - usr

57
我正在更新一个库,其API表面是在.NET 3.5中构建的。因此,所有方法都是同步的。我不能更改API(即将返回值转换为Task),因为这将需要所有调用者进行更改。所以我只能想办法以最佳方式以同步方式调用异步方法。
没有通用的“最佳”方法来执行sync-over-async反模式。只有各种各样的黑客技巧,每种方法都有自己的缺点。
我建议您保留旧的同步API,并在其旁引入异步API。您可以使用“布尔参数hack”,如我在Brownfield Async上的MSDN文章中所述来实现此目的。
首先,简要解释一下您的示例中每种方法的问题:
  1. ConfigureAwait只有在有await时才有意义;否则,它什么也不做。
  2. Result会将异常包装在AggregateException中;如果必须阻塞,使用GetAwaiter().GetResult()代替。
  3. Task.Run将在线程池线程上执行其代码(显然)。这只有在代码可以在线程池线程上运行时才可以使用。
  4. RunSynchronously是一种高级API,仅在极为罕见的动态任务并行性情况下使用。你完全不在那种情况下。
  5. 对单个任务使用Task.WaitAll与仅使用Wait()相同。
  6. async () => await x只是说() => x的一种效率较低的方式。
  7. 从当前线程启动的任务上阻塞可能导致死锁

以下是说明:

// 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;

不要使用上述任何一种方法,因为您已经有了现有的同步代码,所以您应该将其与新的自然异步代码一起使用。例如,如果您现有的代码使用了WebClient

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

如果您想添加一个异步API,我会这样做:
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);

或者,如果你一定要出于某种原因使用 HttpClient:

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

使用这种方法,您的逻辑将进入 Core 方法中,该方法可以同步或异步运行(由 sync 参数确定)。如果 sync true ,则核心方法必须返回一个已完成的任务。对于实现,使用同步API来同步运行,并使用异步API来异步运行。
最终,我建议弃用同步API。

1
你能更详细地解释第六项吗? - Emerson Soares
正如我在我的异步介绍中所解释的那样,你可以await方法的结果,因为它返回的是Task,而不是因为它是async。这意味着对于简单的方法,你可以省略关键字 - Stephen Cleary
我最近在Asp.net MVC 5 + EF6中遇到了同样的问题。我采用了这个回答建议的方法:https://dev59.com/Dmox5IYBdhLWcg3wOx5i#25097498,它对我有帮助: ),但不确定其他情况是否适用。 - LeonardoX
@StephenCleary,为了明确起见:在您自己的Nito.AsyncEx库的WaitAndUnwrapException()中,您只需调用task.GetAwaiter().GetResult(),而不需要将其包装在Task.Run()中。除非最近有任何重大变化,否则我想这意味着它容易出现死锁?是否存在任何上下文环境,您看到该风险?WCF?ASP.Net Web API?控制台?Windows服务?其他? - tsemer
1
将来自英语的文本翻译成中文: 这个答案相当有用,几乎七年后。谢谢! - Mmm
显示剩余6条评论

2

我刚刚遇到了AWS S3 SDK的这个问题。以前是同步的,我建立了很多代码,但现在变成了异步的。没关系:他们改变了它,抱怨也没有什么好处,继续前进。
所以我需要更新我的应用程序,我的选择是要么重构应用程序的大部分为异步,要么“黑客”S3异步API使其行为像同步。
我最终会开始更大的异步重构——有很多好处——但今天我有更重要的事情要做,所以我选择伪造同步。

原始同步代码是
ListObjectsResponse response = api.ListObjects(request);
一个非常简单的异步等效方法适用于我
Task<ListObjectsV2Response> task = api.ListObjectsV2Async(rq2);
ListObjectsV2Response rsp2 = task.GetAwaiter().GetResult();

虽然我知道纯粹主义者可能会批评我,但现实是这只是许多紧迫问题之一,我时间有限,所以我需要做出权衡。完美吗?不是。能用吗?可以。


-5

您可以从非异步方法调用异步方法。请查看下面的代码。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接