向函数添加“异步”关键字时是否有任何缺点

本文关键字:是否 任何 缺点 关键字 函数 添加 异步 | 更新日期: 2023-09-27 18:18:59

当我尝试在我的项目中使用 async/await 时,最后发现几乎所有函数都有 async 关键字,因为当它们等待时,其他异步函数本身应该是异步的。

例如,当我在某些异步 IO 库中使用函数时:

async Task Foo(){
    await file.ReadAsyc();
}

那么调用 Foo(( 的函数也应该是异步的。

async Task Bar() {
    await Foo();
}

最终,我发现很多函数都有异步关键字。

是否有任何缺点(性能受损或其他(?

向函数添加“异步”关键字时是否有任何缺点

当方法标记为 async 时,将在代码中生成状态机。此状态机包含大量代码。当您编写"真正的"并发代码时,这应该不是问题,因为使用 async 和 await 的好处应该超过额外代码的成本。但是,用异步标记库中的每个函数是没有意义的,并且会导致代码膨胀。此外,如果异步方法没有等待,编译器将生成警告。

所以,是的,使用异步有一个"缺点",因为为状态机生成了额外的代码。但是,如果您正在编写并发代码,这不应该成为避免使用 async 和 await 的理由。

为了进行比较,这里是空函数的 IL。

没有异步:

void F() { }
F:IL_0000:ret

使用异步:

async void F() { }
F:IL_0000: ldloca.s 00IL_0002: ldarg.0    IL_0003: stfld UserQuery+d__0.4__thisIL_0008: ldloca.s 00IL_000A:调用System.Runtime.CompilerServices.AsyncVoidMethodBuilder.CreateIL_000F: stfld UserQuery+d__0.t__builderIL_0014: ldloca.s 00IL_0016: ldc.i4.m1  IL_0017: stfld UserQuery+d__0.1__stateIL_001C: ldloca.s 00IL_001E: ldfld UserQuery+d__0.t__builderIL_0023:STLOC.1    IL_0024: ldloca.s 01IL_0026: ldloca.s 00IL_0028:调用System.Runtime.CompilerServices.AsyncVoidMethodBuilder.StartIL_002D:ret        d__0.移动下一页:IL_0000:LDC.i4.1   IL_0001: stloc.0    IL_0002:请假IL_001BIL_0004:STLOC.1    IL_0005: ldarg.0    IL_0006:ldc.i4.s FEIL_0008: stfld UserQuery+d__0.1__stateIL_000D:LDARG.0    IL_000E: ldflda UserQuery+d__0.t__builderIL_0013:LDLOC.1    IL_0014:调用System.Runtime.CompilerServices.AsyncVoidMethodBuilder.SetExceptionIL_0019:离开IL_002EIL_001B: ldarg.0    IL_001C:ldc.i4.s FEIL_001E: stfld UserQuery+d__0.1__stateIL_0023: ldarg.0    IL_0024: ldflda UserQuery+d__0.t__builderIL_0029:调用 System.Runtime.CompilerServices.AsyncVoidMethodBuilder.SetResultIL_002E:ret        d__0.设置状态机:IL_0000: ldarg.0    IL_0001: ldflda UserQuery+d__0.t__builderIL_0006:LDARG.1    IL_0007:调用System.Runtime.CompilerServices.AsyncVoidMethodBuilder.SetStateMachineIL_000C:ret        
状态

机还消耗少量存储来跟踪状态,但这无关紧要。

是否有任何缺点(性能受损或其他(?

通常,如果使用真正的异步操作(通常为 I/O 绑定(,则异步造成的任何性能下降都由更大的可伸缩性和/或响应更快的 UI 弥补。最重要的关键是:仅当您通过性能分析证明性能是必须解决的问题时,才优化性能

正如其他人所指出的,async确实会导致生成状态机,该状态机占用内存并具有更大的代码大小。有关性能影响的最佳资源是 Stephen Toub 的 MSDN 文章和 Channel9 视频。

请注意,您可以采取一种"廉价"优化:

async Task FooAsync() {
  await file.ReadAsync();
}

与以下相同:

Task FooAsync() {
  return file.ReadAsync();
}

当您执行方法重载之类的操作并且不想多次添加async开销时,这是一个很好的技巧。但是,如果该方法中有其他代码,则可能只想使用 asyncawait

让我们看一下 MSDN 对async修饰符的定义:

使用异步修饰符指定方法、lambda 表达式或匿名方法是否异步。如果在方法或表达式上使用此修饰符,则称为异步方法。

他们继续说:

该方法同步运行,直到到达其第一个 await 表达式,此时该方法将挂起,直到等待的任务完成。同时,控制权返回到方法的调用方,如下一节中的示例所示。

如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行。编译器警告会提醒你注意不包含 await 的任何异步方法,因为这种情况可能表示错误。

async关键字在与await结合使用时发挥作用。这是一个标志,告诉编译器"此方法应编译为状态机,因此控件可以返回给调用方并在异步操作完成后继续,并恢复其余代码作为延续"。

如果方法中不存在await关键字,它将简单地从头到尾同步运行,但状态的代码将被发出,这是一个重要因素。

现在,当等待async方法时,编译器会生成一个状态机。这确实是有代价的,尽管框架团队确保它对代码性能的影响最小。更重要的是,在幕后,当您await时流动的SynchronizationContext(除非明确说明不要这样做(,它负责将延续封送回原始线程。

有关内部的更多信息,尤其是性能方面,请参阅异步性能:了解异步和 Await 的成本

缺点是使用这些基于异步模式的方法的代码必须知道它们是异步工作的。也就是说,调用异步方法的每个方法也必须转换为异步方法。

虽然 async/await 模式简化了异步编程,因为代码看起来像同步代码,但开发人员需要有关多线程的高级知识,以确保他们的代码不会陷入死锁、同步问题和意外行为。

例如,由 IIS 和 ASP.NET 托管的代码将需要特殊的专业知识(ASP.NET 异步页面、异步任务......(,因为 IIS 进程模型的行为与 Windows 上的常规可执行应用程序不同。

最后,如果我们谈论的是简单的客户端应用程序(您的应用程序可能有无用的线程同步开销(,那么多线程环境中的线程同步可能会降低性能,但是一旦客户端应用程序需要大量的 I/O 和并行化,异步编程将更好地工作,因为您将利用多核 CPU。如果我们谈论 GUI 应用程序,异步编程将是避免在执行长时间运行的进程时阻塞 UI 的好方法。