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

时间:2016-10-29 22:25:47

标签: c# asp.net multithreading asynchronous task-parallel-library

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

我可能不够清楚 - 情况是我现有的代码不是异步识别的,我想使用新的库,如System.Net.Http和仅支持异步方法的AWS SDK。所以我需要缩小差距,并且能够拥有可以同步调用的代码,但之后可以在其他地方调用异步方法。

我已经做了很多阅读,并且有很多时候已经被要求和回答。

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#?

问题在于大多数答案都不同!我见过的最常见的方法是使用.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;
    }

4 个答案:

答案 0 :(得分:66)

  

所以我离开了如何以同步方式最好地调用异步方法。

首先,这是一件好事。我之所以这样说是因为在Stack Overflow中常见的是将其视为魔鬼的契约,而不考虑具体案例。

阻止异步以使其同步具有可能重要或可能完全无关的性能成本。这取决于具体案例。

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

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

RunSynchronously无效,因为并非所有任务都可以通过这种方式处理。

.GetAwaiter().GetResult()Result/Wait()的不同之处在于它模仿await异常传播行为。您需要决定是否需要。 (因此,研究这种行为是什么;不需要在这里重复。)

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

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

答案 1 :(得分:26)

  

我正在更新具有在.NET 3.5中构建的API表面的库。因此,所有方法都是同步的。我无法更改API(即将返回值转换为Task),因为这需要所有调用者都更改。所以我离开了如何以同步方式最好地调用异步方法。

没有普遍的&#34;最好的&#34;执行异步同步反模式的方法。只有各种各样的黑客都有各自的缺点。

我建议您保留旧的同步API,然后引入异步API。您可以使用"boolean argument hack" as described in my MSDN article on Brownfield Async

执行此操作

首先,简要解释一下您示例中每种方法的问题:

  1. ConfigureAwait只有在await时才有意义;否则,它什么都不做。
  2. Result将在AggregateException中包含例外;如果您必须阻止,请改用GetAwaiter().GetResult()
  3. Task.Run将在线程池线程上执行其代码(显然)。只有代码可以在线程池线程上运行时,这很好
  4. RunSynchronously是在执行基于动态任务的并行操作时极少数情况下使用的高级API。你根本不在那种情况下。
  5. 只有一项任务的
  6. Task.WaitAllWait()相同。
  7. async () => await x只是一种效率较低的说法() => x
  8. 阻止从当前线程can cause deadlocks开始的任务。
  9. 以下是细分:

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

答案 2 :(得分:1)

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

原始同步代码是
ListObjectsResponse response = api.ListObjects(request);
,而一个对我有用的非常简单的异步等效
{{ 1}}
Task<ListObjectsV2Response> task = api.ListObjectsV2Async(rq2);

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

答案 3 :(得分:-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;
    }