在 C#
中,async void
方法是许多开发人员写异步等待代码时遇到许多问题的根源。我们被建议使用的模式当然是异步任务(async Task
),但是有些情况 - 比如在 C#
中的事件处理程序 - 方法签名就是不兼容的。
在本文中,我将解释为什么要避免在 C#
中使用 async void
方法。我们将涵盖一些代码示例,比较 async void
和 async Task
以便更好地理解,我还将解释如果您没有其他选择而只能使用 async void
时应该怎么做。
C#
中的 async void
方法是什么?
在 C#
中,async void
方法是一种定义异步方法但不返回值的方法。这些方法通常用于事件处理程序或其他不支持 Task
返回类型而强制使用 void
的场景中。
async void
方法通过在方法签名之前使用 async
关键字,然后是返回类型 void
来定义。例如:
public async void SomeMethod()
{
// Code here
}
与返回 Task
或 Task<T>
的常规异步方法相比,有几个重要的区别需要注意。当我说 “重要” 时,我的意思是 “你真的需要尽可能避免这种情况,这样你就能避免一些头痛的事情” 。
async void
方法与 async Task
之间的区别
async void
方法和 async Task
方法之间的主要区别之一在于异常处理方式。
在 async Task
方法中,发生的任何异常都会被返回的 Task
对象捕获。这允许调用代码处理异常或等待任务以后观察任何异常。这就是 C#
中整个异步等待基础架构的构建方式。
另一方面,async void
方法无法直接被等待,并且其中发生的任何异常都将冒泡到哪里呢?首先启动异步方法的 SynchronizationContext
。即使是来自 Microsoft
的 Stephen Cleary
在他的 ^文章 中也提到:
对于
async void
方法,没有Task
对象,因此从async void
方法抛出的任何异常将直接在启动async void
方法时活动的SynchronizationContext
上引发。图 2 说明了从async void
方法中抛出的异常无法自然地捕获。——Stephen Cleary
C# 中 async Task 与 async void 方法的代码示例
比较使用每种模式的相同代码布局的两个变体将有助于了解问题如何产生。考虑以下示例,其中使用了 async Task
:
public async Task ProcessDataAsync()
{
// Some asynchronous operations
}
public async void HandleButtonClick(object sender, EventArgs e)
{
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
// Handle the exception
}
}
在这段代码中,如果 ProcessDataAsync
事件处理程序方法中发生异常,它将被 HandleButtonClick
方法中的 try-catch
块捕获,并且可以得到适当处理。然而,如果 ProcessDataAsync
方法定义为 async void
,任何由 ProcessDataAsync
抛出的异常都会绕过 HandleButtonClick
事件处理程序方法中的 catch
块,并且可能导致应用程序崩溃。
public async void ProcessDataAsync()
{
// Some asynchronous operations
}
public async void HandleButtonClick(object sender, EventArgs e)
{
try
{
ProcessDataAsync();
}
catch (Exception ex)
{
// This will never catch the async exceptions!
}
}
C# 中 async void 方法的危险
在本文中,你希望注意到的一个共同主题是,C#
中的 async void
方法是危险的,你应该尽量避免使用它。以下是我们在 async void
方法中遇到的挑战列表,希望能帮助你远离它们(除非你别无选择):
- 错误传播:
async void
方法不允许错误被捕获或传播。当在这样的方法内部发生异常时,它会逃逸到同步上下文,通常导致无法处理的异常,可能会导致应用程序崩溃。 - 等待行为:与
async Task
方法不同,async void
方法无法被等待。这可能会导致控制异步操作流程时出现问题,可能会引发竞态条件或按照意图之外的顺序执行操作。 - 调试困难:在
async void
方法中调试异常更具挑战性,因为调用堆栈可能无法准确表示异常抛出时的执行流程,这使得识别和修复错误的过程变得更加复杂。
在 C# 中处理 async void 方法的最佳实践
在 C#
中处理 async void
方法时,重要的是要意识到它们的潜在危险,并遵循最佳实践,以确保代码库健壮可靠。以下是谨慎处理 async void
方法的一些建议:
尽可能避免使用
async void
:通常应尽量避免使用async void
方法,特别是在异常处理和错误恢复至关重要的场景中。虽然async void
方法可能看起来很方便,但它们缺乏传播异常的能力,可能导致不可预测的程序行为。相反,考虑使用async Task
方法,它们提供更好的错误处理能力。改用
async Task
:通过使用async Task
返回类型的异步方法,您可以利用Task
内置的异常处理机制。这使您能够适当地捕获和处理异常,确保您的代码控制执行流程。使用async Task
方法还可以提高代码可维护性、可测试性,并减少async void
方法带来的神秘问题。处理
async void
方法中的异常:如果必须使用async void
方法,则重要的是要正确处理异常,以防止它们静默传播并导致意外的系统行为。一种方法是将代码置于try/catch
块中。在catch
块中,您可以记录异常并相应地处理,例如向用户显示错误消息或回滚任何相关操作。记录和监控异步操作:在处理
async void
方法时,日志记录和监控变得更加关键。由于这些方法没有返回类型,确定它们的完成或识别任何潜在问题变得具有挑战性。实施健壮的日志记录和监控系统,例如使用Serilog
等日志框架或使用应用程序洞察,可以帮助跟踪您的异步操作的进度和状态,有助于调试和故障排除。
在 C# 中使用 try/catch
处理 async void
方法
确保将每个 async void
方法主体包装在 try/catch
中是最直接的方法。也许有人可以创建一个 Rosyln
分析器来强制执行这一点?
以下是一个示例,演示了如何在整个 async void
方法的代码主体周围使用 try-catch
:
public async void DoSomethingAsync()
{
try
{
// Perform asynchronous operations
await Task.Delay(1000);
await SomeAsyncMethod();
}
catch (Exception ex)
{
// TODO: ... whatever you need to do to properly report
// on issues in your async void calls so that you
// can debug them more effectively.
// Log the exception and handle it appropriately
Logger.Error(ex, "An error occurred while executing DoSomethingAsync");
// Display an error message or take necessary action
DisplayErrorMessage("Oops! Something went wrong. Please try again later.");
}
}
遵循这些最佳实践,您可以减轻 async void
方法相关的风险。请记住,尽可能优先使用 async Task
方法,以便更好地处理异常并控制异步执行流程 - 除非是绝对的最后选择,否则真的不要在任何地方添加 async void
。
总结 async void 方法在 C# 中的使用
总而言之,在 C#
中,async void
方法之所以会存在危险,有几个原因,您会希望优先考虑不使用它们。有些不幸的情况下,API
和方法签名不匹配,比如事件处理程序,但除此之外,请尽力避免使用这些方法。
通过使用 async void
,我们失去了等待或正确处理异常的能力。这可能会导致未处理的异常和代码中的意外行为。为了避免这些危险,我建议您尽可能使用 async Task
方法来处理那些打算异步执行的方法。这使我们能够等待结果,处理异常,并更好地控制代码的执行流程。它促进了更好的错误处理,并提高了整体代码质量。当您别无选择时,请确保将整个 async void
方法包装在 try/catch
中,并加入适当的错误处理/记录/报告。
Comments