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

Eri*_*k T 62 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.所以我需要缩小差距,并且能够拥有可以同步调用的代码,然后可以在其他地方调用异步方法.

我已经做了很多阅读,并且有很多次这已经被问及并回答了.

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

同步等待异步操作,以及为什么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;
    }
Run Code Online (Sandbox Code Playgroud)

usr*_*usr 75

所以我留下了如何以同步方式最好地调用异步方法.

首先,这是一件好事.我正在陈述这一点,因为在Stack Overflow上常见的是将其视为魔鬼的行为作为一揽子陈述而不考虑具体案例.

为了正确性,不需要一直异步.阻止异步以使其同步具有可能重要或可能完全无关的性能成本.这取决于具体情况.

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

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

RunSynchronously 使用无效,因为并非所有任务都可以这样处理.

.GetAwaiter().GetResult()不同之处Result/Wait()在于它模仿await异常传播行为.您需要决定是否需要.(所以研究一下这种行为;不需要在这里重复它.)

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

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

  • 谢谢,这很有道理. (3认同)

Ste*_*ary 34

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

没有通用的"最佳"方式来执行异步同步反模式.只有各种各样的黑客各有各的缺点.

我建议您保留旧的同步API,然后引入异步API.你可以使用我在MSDN上关于Brownfield Async的文章中描述"boolean argument 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;
Run Code Online (Sandbox Code Playgroud)

由于您具有现有的工作同步代码,因此您应该将它与新的自然异步代码一起使用,而不是任何这些方法.例如,如果您使用现有代码WebClient:

public string Get()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}
Run Code Online (Sandbox Code Playgroud)

并且您想要添加异步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);
Run Code Online (Sandbox Code Playgroud)

或者,如果您因某些原因必须使用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);
Run Code Online (Sandbox Code Playgroud)

使用这种方法,您的逻辑将进入Core方法,这些方法可以同步或异步运行(由sync参数确定).如果synctrue,则核心方法必须返回已完成的任务.对于实现,使用同步API同步运行,并使用异步API异步运行.

最后,我建议弃用同步API.