从同步代码中异步调用C#方法:方法之争

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

如何在C#中从同步代码调用异步方法?干货满满,绝不注水



想象一下:你晚上正在写代码,完善一个旧的C#控制台工具。一切工作都是同步的,像时钟一样精准。然后——砰!——你需要调用一个只通过async Task实现的API。难道要重写整个程序?是不是很熟悉这种痛苦?



“如何在C#中从同步方法调用异步方法”这个问题是Stack Overflow上最热门的问题之一。这并非没有原因。在评论中,有大量的建议,从“直接用.Result,别担心”到“你疯了吗?会死锁的!”。让我们逐一分析:哪些方法真的有效,哪些是通往地狱之路。



为什么简单的“await”不起作用?



如果你尝试这样写:

public void SyncMethod(){    await DoSomethingAsync(); // 错误 CS4032}

编译器会直接拒绝编译。await只能用于标记了async修饰符的方法内部。而async void则是另一回事,我们也会讨论。



那么,我们有一个异步方法:

public async Task<string> GetDataAsync(){    using var httpClient = new HttpClient();    return await httpClient.GetStringAsync("https://api.example.com/data");}


还有一个同步方法,我们想从中调用它。让我们看看各种方案。



方法1:GetAwaiter().GetResult() —— 黑客风格



最流行,也许也是最优雅的方式:

public string CallSync(){    return GetDataAsync().GetAwaiter().GetResult();}


为什么这很酷:

  • 不会生成AggregateException异常(与.Result不同)。如果异步方法内部发生错误,你将得到原始的错误类型,而不是包装后的异常。
  • 代码可读性好,无需额外的try-catch。


但有一个问题:如果你的同步方法在带有SynchronizationContext的上下文中执行(例如,WinForms或WPF的UI线程),你可能会遇到死锁——线程相互阻塞。线程A等待异步任务完成,而任务无法完成,因为它正在等待线程A释放。糟糕透了。



如何避免:在异步方法内部使用.ConfigureAwait(false)。但这会改变API,而不仅仅是调用代码。



方法2:Task.Run(async () => await ...).GetAwaiter() —— 转移到线程池



如果你无法修改异步方法,但想确保避免死锁——将任务转移到线程池:

public string CallSyncSafe(){    return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();}


神奇之处:Task.Run在ThreadPool的线程上启动委托,该线程没有SynchronizationContext。死锁不可能发生。即使原始方法使用了ConfigureAwait(true)。



缺点:额外的上下文切换。对于数百次调用来说不理想。对于一次性调用来说完美。



方法3:.Result 或 .Wait() —— 老派方法



public string CallSyncOldSchool(){    return GetDataAsync().Result; // 或者 .Wait()}


为什么不好:

  • 发生错误时会抛出AggregateException。要获取真正的错误,需要深入InnerException。
  • 在UI上下文中仍然存在死锁风险。
  • 代码看起来像是2012年的临时解决方案。


这种方法在代码审查中是个危险信号。如果我在拉取请求中看到这个,我会打回去。



方法4:AsyncHelper 或自定义 RunSync



经验丰富的开发人员有时会编写自己的辅助方法。例如,像这样:

                

博客

书籍推荐