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


75

我正在更新具有.NET 3.5中内置的API表面的库。结果,所有方法都是同步的。我无法更改API(即,将返回值转换为Task),因为这将要求所有调用者都进行更改。因此,我剩下如何以同步方式最好地调用异步方法了。这是在ASP.NET 4,ASP.NET Core和.NET / .NET Core控制台应用程序的上下文中。

我可能还不够清楚-这种情况是我有不支持异步的现有代码,并且我想使用仅支持异步方法的新库,例如System.Net.Http和AWS开发工具包。因此,我需要弥合差距,并能够拥有可以被同步调用但可以在其他地方调用异步方法的代码。

我已经读了很多书,而且有很多次被问及回答了。

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

同步等待异步操作,为什么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
答案是您不这样做。您添加了异步的新方法,并为旧的调用者保留了旧的同步方法。
斯科特·张伯伦

14
这似乎有些苛刻,并且确实扼杀了使用新代码的能力。例如,新版本的AWS开发工具包没有非异步方法。其他许多第三方库也是如此。因此,除非您重写世界,否则您将无法使用其中任何一个?
Erick T

选项8:也许TaskCompletionSource是一个选项?
OrdinaryOrange

仅当Task仍在运行时,Result属性才导致死锁。我不确定100%,但是如果您正确地等到任务结束就应该安全了,就像方法7一样
infiniteRefactor

1
显示为什么不能从异步代码使用同步API的示例可能会有助于找到比@scott更好的答案
Alexei Levenkov

Answers:


89

因此,我剩下如何以同步方式最好地调用异步方法了。

首先,这是一件可以做的事情。我之所以这样说是因为在Stack Overflow上通常会将其指出为恶魔的行为,作为笼统的声明而不考虑具体情况。

不需要为了保持正确性而一直保持异步。阻止异步使其同步具有一定的性能成本,该成本可能很重要或完全不相关。这取决于具体情况。

死锁来自试图同时进入同一单线程同步上下文的两个线程。避免这种情况的任何技术都可以可靠地避免由于阻塞而导致的死锁。

在这里,您所有的呼叫.ConfigureAwait(false)都没有意义,因为您没有等待。

RunSynchronously 之所以无效,是因为并非所有任务都可以那样处理。

.GetAwaiter().GetResult()区别Result/Wait()在于它模仿await异常传播行为。您需要确定是否想要。(因此,请研究该行为是什么;无需在此重复。)

除此之外,所有这些方法都具有相似的性能。他们将以一种或另一种方式分配OS事件并对其进行阻止。那是昂贵的部分。我不知道哪种方法绝对便宜。

我个人喜欢这种Task.Run(() => DoSomethingAsync()).Wait();模式,因为它可以避免死锁,它很简单,并且不会隐藏一些GetResult()可能隐藏的异常。但是您也可以GetResult()与此一起使用。


3
谢谢,这很有意义。
Erick T

41

我正在更新具有.NET 3.5中内置的API表面的库。结果,所有方法都是同步的。我无法更改API(即,将返回值转换为Task),因为这将要求所有调用者都进行更改。因此,我剩下如何以同步方式最好地调用异步方法了。

没有通用的“最佳”方法来执行异步同步反模式。只有各种各样的黑客都有各自的缺点。

我建议您保留旧的同步API,然后在它们旁边引入异步API。您可以使用我在Brownfield Async上的MSDN文章中所述“布尔参数hack”来执行此操作。

首先,简要说明示例中每种方法的问题:

  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参数确定)。如果synctrue,则核心方法必须返回一个已完成的任务。为了实现,请使用同步API来同步运行,并使用异步API来异步运行。

最终,我建议弃用同步API。


您能否再解释一下第六项?
艾默生·苏亚雷斯

@EmersonSoares:正如我在异步介绍中所解释的,您可以await得到方法的结果,因为它返回了Task,而不是因为它是async。这意味着对于平凡的方法,您可以删除关键字
史蒂芬·克利西

我最近在asp.net mvc 5 + EF6中遇到了同样的问题。我使用TaskFactory,因为这个答案表明stackoverflow.com/a/25097498/1683040,这对我来说是个窍门:),但不确定其他情况。
LeonardoX

我了解您使用的建议WebClient。但是,使用Core方法有什么好处HttpClient?如果同步方法直接调用该GetCoreSync方法,将布尔参数hack排除在这种情况下,会更干净,更高效吗?
Creepin

@Creepin:是的,我相信。
斯蒂芬·克莱里

1

我刚刚使用AWS S3 SDK进行了此操作。过去是同步的,我在上面建立了很多代码,但是现在是异步的。没关系:他们改变了它,抱怨不了,继续前进。
因此,我需要更新我的应用程序,并且我的选择是重构我的应用程序的大部分以使其异步,或者“破解” S3异步API以使其表现得像同步。
我最终将讨论更大的异步重构-有很多好处-但是今天我要炸更大的鱼,所以我选择了伪造同步。

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

尽管我知道纯粹主义者可能为此嘲笑我,但现实是,这只是许多紧迫问题之一,我的时间有限,因此我需要权衡取舍。完善?不行吗 是。


-3

您可以从非异步方法调用异步方法。检查以下代码。

     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;
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.