同步方法中调用异步方法的最优实现方案探讨
现有代码示例
异步方法
public Task<Customer> Get(int id) { return await client.GetCustomer(id); }
需要调用异步方法的同步方法
public Order GetOrder() { // 调用异步方法并定义Customer变量 return new Order { Customer = customer}; }
问题:实现该调用的最佳方式是什么?
各实现方式分析
第一种方式:
Get(10).Result
直接调用Result会阻塞当前线程,在存在同步上下文(如ASP.NET老版本、WinForms/WPF)的场景下极易引发死锁——异步方法等待同步上下文时,被阻塞的线程正占用该上下文,导致互相等待。此外,抛出的异常会被包装成AggregateException,增加异常处理复杂度。第二种方式:
Get(10).GetAwaiter().GetResult()
同样会阻塞线程,但抛出的是原始异常而非AggregateException,异常处理更直接。但死锁风险依然存在,未解决同步上下文冲突的核心问题。第三种方式:
Task.Run(async()=>await Get(10)).Result
通过Task.Run将异步方法放到线程池执行,避开当前线程的同步上下文,能避免死锁。但缺点是额外占用线程池资源,若异步方法依赖当前上下文(如HttpContext)会导致上下文丢失,且线程切换存在性能开销。第四种方式:AsyncHelper.RunSync
微软官方的AsyncHelper通过捕获并合理处理同步上下文,在异步执行后恢复上下文,兼顾死锁避免与上下文保留。但它是为特定场景(如ASP.NET Identity)设计的,需自行引入代码,并非通用全能方案。第五种方式:AsyncContext.Run
Nito.AsyncEx库的AsyncContext.Run会创建独立同步上下文执行异步方法,既隔离原上下文避免死锁,又能让异步方法在可控环境中运行,是成熟的第三方解决方案,无需自行维护复杂的上下文处理逻辑。
最佳实践结论
优先推荐:改造同步方法为异步
从设计层面,异步代码应配套异步调用链,将GetOrder改为async Task<Order> GetOrderAsync(),直接await Get(10),这是最符合异步编程模型的方式,从根源上避免同步调用异步的各类问题。若无法改造同步方法
- 若已引入Nito.AsyncEx库,优先使用AsyncContext.Run,适配绝大多数场景,安全且封装完善。
- 若不想引入第三方库,可使用AsyncHelper.RunSync(自行引入代码);或仅在无同步上下文的环境(如控制台程序)中使用
GetAwaiter().GetResult(),有上下文时需搭配Task.Run避免死锁,但要注意上下文丢失问题。
内容的提问来源于stack exchange,提问作者Anton

