C# 非async函数调用async函数的正确姿势与避坑指南

1次阅读
没有评论

共计 2083 个字符,预计需要花费 6 分钟才能阅读完成。

image.webp

背景与痛点

在 C# 开发中,我们经常需要在非异步方法中调用异步方法。这种情况特别常见于以下几种场景:

C# 非 async 函数调用 async 函数的正确姿势与避坑指南

  • 旧代码改造时,无法立即将所有方法改为 async
  • 程序入口点如 Main 方法,在 C# 7.1 之前不支持 async
  • 第三方库或框架强制要求同步接口实现

常见的错误做法是直接使用.Result 或.Wait(),这会导致以下问题:

  1. 死锁风险:当在 UI 线程或 ASP.NET 请求上下文中同步等待异步任务时,可能导致死锁
  2. UI 冻结:在 WinForms/WPF 应用中阻塞 UI 线程会导致界面无响应
  3. 线程饥饿:在 ASP.NET 中可能耗尽线程池线程

技术方案对比

不推荐:.Result/.Wait()

// 危险!可能导致死锁
var result = GetDataAsync().Result;

问题在于这会同步阻塞调用线程,同时尝试在同步上下文中继续执行异步操作的后半部分。

改进:.GetAwaiter().GetResult()

// 稍好,但仍可能阻塞线程
var result = GetDataAsync().GetAwaiter().GetResult();

相比.Result,这种方式能保留原始异常堆栈,但仍存在同步阻塞问题。

推荐:Task.Run(适用于 UI 应用)

// WinForms/WPF 中的安全调用方式
var result = Task.Run(async () => await GetDataAsync()).Result;

这种方法将异步操作转移到线程池线程执行,避免阻塞 UI 线程。

代码示例

控制台应用

// 错误示范
static void Main() {var data = GetDataAsync().Result; // 可能导致死锁
    Console.WriteLine(data);
}

// 正确做法 (C# 7.1+)
static async Task Main() {var data = await GetDataAsync();
    Console.WriteLine(data);
}

// 旧版 C# 解决方案
static void Main() {var data = Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();
    Console.WriteLine(data);
}

WinForms 应用

// 错误示范 - 冻结 UI
private void button1_Click(object sender, EventArgs e) {var data = GetDataAsync().Result;
    textBox1.Text = data;
}

// 正确做法 1 - 使用 async void(仅限事件处理器)private async void button1_Click(object sender, EventArgs e) {var data = await GetDataAsync();
    textBox1.Text = data;
}

// 正确做法 2 - 使用 Task.Run
private void button1_Click(object sender, EventArgs e) {Task.Run(async () => {var data = await GetDataAsync();
        this.Invoke(() => textBox1.Text = data);
    });
}

ASP.NET Core

// 错误示范 - 可能导致线程饥饿
public IActionResult Get() {var data = GetDataAsync().Result;
    return Ok(data);
}

// 正确做法
public async Task<IActionResult> Get() {var data = await GetDataAsync();
    return Ok(data);
}

生产环境考量

async void 的危险

async void 方法有以下特点:

  1. 无法等待其完成
  2. 异常会直接崩溃进程(在非 UI 上下文中)
  3. 仅适用于事件处理器

性能考量

同步阻塞异步调用比纯异步调用多出约 30% 的开销,因为需要额外的线程和上下文切换。

异常处理

同步调用异步方法时,AggregateException 会被抛出。需要特别注意处理方式:

try {var result = GetDataAsync().Result;
} catch (AggregateException ex) {// 实际异常在 ex.InnerException 中}

避坑指南

  1. 黄金法则 1 :不要在 ASP.NET 请求上下文中使用.Result/.Wait()
  2. 黄金法则 2 :UI 事件处理器可以使用 async void,其他情况避免使用
  3. 黄金法则 3 :控制台应用考虑升级到 C# 7.1+ 使用 async Main
  4. 黄金法则 4 :无法避免同步阻塞时,使用 Task.Run 包裹异步调用
  5. 黄金法则 5 :始终优先考虑完全异步调用链

延伸思考

  1. 在大型遗留系统中,如何设计渐进式的异步改造方案?
  2. 对于必须提供同步 API 的库,如何设计既能满足同步调用需求又能保持高效的双重实现?

总结

在非 async 方法中调用 async 方法需要特别注意执行上下文和线程模型。理解这些底层机制后,我们可以根据具体场景选择合适的调用方式。记住:能异步就异步,必须同步时谨慎处理。

正文完
 0
评论(没有评论)