C# 微软async开发者团队关于async/await常见问题的解答


勇哥注:

文章很好,可惜部分链接已经失效了。


有时,我会收到来自开发人员的问题,这些问题强调需要更多关于 C# 和 Visual Basic 中新的“async”和“await”关键字的信息。我一直在对这些问题进行分类,我想我会借此机会分享我对这些问题的答案。

概念概述

我在哪里可以很好地了解 async/await 关键字?

通常,您可以在https://msdn.com/async的 Visual Studio 异步页面上找到大量资源(文章、视频、博客等的链接)为了仅列出一些特定资源,MSDN 杂志 2011 年 10 月号包括三篇文章,很好地介绍了该主题。如果您阅读了所有内容,我建议您按以下顺序阅读:

  1. 异步编程:使用新的 Visual Studio Async CTP 更轻松地进行异步编程

  2. 异步编程:使用 Await 暂停和播放

  3. 异步编程:了解 Async 和 Await 的成本

.NET 团队博客还很好地概述了 .NET 4.5 中的异步: 4.5 中的异步:值得等待

为什么我需要编译器来帮助我进行异步编程?

Anders Hejlsberg在 //BUILD/上的 C# 和 Visual Basic未来方向演讲提供了一个很好的导览,解释了为什么编译器在这里真正有益。简而言之,当您使用回调和继续传递样式手动反转控制流时,编译器承担了进行复杂转换的责任,否则您将手动进行。您可以使用语言的控制流构造来编写代码,就像编写同步代码一样,并且幕后编译器应用必要的转换来使用回调以避免阻塞线程。

为了实现异步的好处,我不能将我的同步方法包装在对 Task.Run 的调用中吗?

这取决于您为什么要异步调用方法的目标。如果您的目标只是将您正在执行的工作卸载到另一个线程,例如,以保持 UI 线程的响应能力,那么当然可以。如果您的目标是帮助实现可伸缩性,那么不,仅在 Task.Run 中包装同步调用无济于事。有关更多信息,请参阅我应该为同步方法公开异步包装器吗?如果您想从 UI 线程将工作卸载到工作线程,并且您使用 Task.Run 来执行此操作,则通常希望在后台工作完成后在 UI 线程上做一些工作,以及这些语言功能使这种协调变得简单而无缝。

“异步”关键字

“async”关键字在应用于方法时有什么作用?

当您使用“async”关键字标记方法时,您实际上是在告诉编译器两件事:

  1. 您告诉编译器您希望能够在方法中使用“await”关键字(当且仅当它所在的方法或 lambda 被标记为异步时,您才能使用 await 关键字)。这样做时,您是在告诉编译器使用状态机编译该方法,以便该方法能够挂起,然后在等待点异步恢复。

  2. 您告诉编译器将方法的结果或可能发生的任何异常“提升”到返回类型中。对于返回 Task 或 Task<TResult> 的方法,这意味着在该方法中未处理的任何返回值或异常都存储到结果任务中。对于返回 void 的方法,这意味着任何异常都会通过方法初始调用时当前的任何“SynchronizationContext”传播到调用者的上下文。

在方法上使用“async”关键字是否会强制该方法的所有调用都是异步的?

不会。当您调用标记为“async”的方法时,它开始在当前线程上同步运行。因此,如果您有一个返回 void 的同步方法,并且您所做的只是将其标记为“async”,则该方法的调用仍将同步运行。无论您将返回类型保留为“void”还是将其更改为“Task”,都是如此。类似地,如果您有一个返回一些 TResult 的同步方法,并且您所做的只是将其标记为“async”并将返回类型更改为“Task<TResult>”,则该方法的调用仍将同步运行。

将方法标记为“异步”不会影响该方法是同步运行还是异步运行完成。相反,它使方法能够被拆分为多个部分,其中一些部分可以异步运行,以便该方法可以异步完成。这些片段的边界只能出现在您使用“await”关键字显式编码的地方,因此如果在方法代码中根本不使用“await”,则只会有一个片段,并且由于该片段将开始运行同步,它(以及它的整个方法)将同步完成。

“async”关键字是否会导致调用方法排队到线程池?创建一个新线程?向火星发射火箭飞船?

不。不。不。请参阅前面的问题。“async”关键字向编译器指示可以在方法内部使用“await”,这样方法可以在等待点挂起,并在等待的实例完成时异步恢复其执行。这就是为什么如果标记为“async”的方法内部没有“await”,编译器会发出警告。

我可以将任何方法标记为“异步”吗?

不可以。只有返回 void、Task 或 Task<TResult> 的方法才能被标记为异步。此外,并非所有此类方法都可以标记为“异步”。例如,您不能使用“async”:

  • 在您的应用程序的入口点方法上,例如 Main。当您等待尚未完成的实例时,执行将返回给方法的调用者。在 Main 的情况下,这将从 Main 返回,从而有效地结束程序。

  • 在归因于的方法上:

    • [MethodImpl(MethodImplOptions.Synchronized)]。有关为什么不允许这样做的讨论,请参阅.NET 4.5 Beta 中并行性的新增功能将方法归类为 Synchronized 类似于用 lock/SyncLock 包装方法的整个主体。

    • [SecurityCritical] 和 [SecuritySafeCritical]。当您编译异步方法时,该方法的实现/主体实际上以编译器生成的 MoveNext 方法结束,但它的属性仍保留在您定义的签名上。这意味着像 [SecuritySafeCritical] 之类的属性(旨在直接影响您在方法主体中能够执行的操作)将无法正常工作,因此它们被禁止,至少目前是这样。

  • 在带有 ref 或 out 参数的方法上。调用者希望在方法的同步调用完成时设置这些值,但实现可能直到异步完成之后才会设置它们。

  • 在用作表达式树的 lambda 上。异步 lambda 表达式无法转换为表达式树。

在编写标记为“异步”的方法时,我应该使用任何约定吗?

是的。基于任务的异步模式 (TAP) 完全专注于如何从库中公开返回 Task 或 Task<TResult> 的异步方法。这包括但不限于使用“async”和“await”关键字实现的方法。要深入了解 TAP,请参阅基于任务的异步模式文档。

我是否需要“启动”由标记为“异步”的方法创建的任务?

不可以。从 TAP 方法返回的任务是“热的”,这意味着任务代表已经在进行中的操作。您不仅不需要在此类任务上调用“.Start()”,而且如果您尝试这样做会失败。有关更多详细信息,请参阅Task.Start 上的常见问题解答

我是否需要“处置”由标记为“异步”的方法创建的任务?

不需要。一般来说,您不需要处理任何任务。请参阅我需要处理任务吗?.

“async”与当前的 SynchronizationContext 有什么关系?

对于返回 Task 或 Task<TResult> 的标记为“async”的方法,没有与 SynchronizationContext 的方法级交互。但是,对于标记为“async”并返回 void 的方法,存在潜在的交互。

当调用“async void”方法时,方法调用的序言(由编译器创建的 AsyncVoidMethodBuilder 处理以表示方法的生命周期)将捕获当前的 SynchronizationContext(“捕获”在这里意味着它访问它并存储它)。如果存在非空 SynchronizationContext,则会影响两件事:

  • 方法调用的开始将导致对捕获上下文的 OperationStarted 方法的调用,并且该方法的执行完成(无论是同步还是异步)将导致对捕获上下文的 OperationCompleted 方法的调用。这使上下文有机会引用计数未完成的异步操作;相反,如果该方法返回了 Task 或 Task<TResult>,则调用者可以通过该返回的任务完成相同的跟踪。

  • 如果该方法由于未处理的异常而完成,则该异常的抛出将被发布到捕获的 SynchronizationContext。这为上下文提供了处理失败的机会。这与 Task 或 Task<TResult> 返回异步方法形成对比,其中异常可以通过返回的任务封送到调用者。

如果在调用“async void”方法时没有 SynchronizationContext,则不会捕获上下文,然后由于没有可调用的 OperationStarted / OperationCompleted 方法,因此不会调用任何上下文。在这种情况下,如果异常未得到处理,则异常会在 ThreadPool 上传播,默认行为将导致进程终止。

“等待”关键字

“await”关键字有什么作用?

“await”关键字告诉编译器将可能的挂起/恢复点插入标记为“async”的方法中。

从逻辑上讲,这意味着当你写“await someObject;”时 编译器会生成代码来检查 someObject 所代表的操作是否已经完成。如果是,则在等待点上同步继续执行。如果没有,生成的代码会将一个延续委托挂接到等待的对象,这样当表示的操作完成时,将调用该延续委托。这个延续委托将重新进入该方法,在前一次调用停止的这个等待位置进行接收。此时,无论等待的对象在等待时是否已经完成,都将提取该对象的任何结果,或者如果操作失败,则将传播发生的任何异常。

在代码中,这意味着当您编写:

等待某个对象;

编译器将其转换为如下内容(此代码是编译器实际生成的近似值):

private class FooAsyncStateMachine : IAsyncStateMachine
{
    // 用于保留“本地”和其他必要状态的成员字段
    int $state;
    TaskAwaiter $awaiter;
    ...
    public void MoveNext()
    {
        // 在恢复时跳转表返回到正确的语句
        switch (this.$state)
        {
            ...
            case 2: goto Label2;
            …
        }
        …
        // “await someObject;”的扩展
        this.$awaiter = someObject.GetAwaiter();
        如果 (!this.$awaiter.IsCompleted)
        {
            this.$state = 2;
            this.$awaiter.OnCompleted(MoveNext);
            返回;
            标签 2:
        }
        this.$awaiter.GetResult();
        ...
    }
}

什么是可等待的?什么是等待者?

虽然 Task 和 Task<TResult> 是两种非常常见的等待类型,但它们并不是唯一可以等待的类型。

“awaitable”是任何公开 GetAwaiter 方法的类型,该方法返回有效的“awaiter”。这个 GetAwaiter 方法可能是一个实例方法(就像在 Task 和 Task<TResult> 的情况下一样),也可能是一个扩展方法。

“awaiter”是从可等待对象的 GetAwaiter 方法返回的任何类型,并且符合特定模式。awaiter 必须实现 System.Runtime.CompilerServices.INotifyCompletion 接口,并且可以选择实现 System.Runtime.CompilerServices.ICriticalNotifyCompletion 接口。除了提供来自 INotifyCompletion 的 OnCompleted 方法的实现(以及来自 ICriticalNotifyCompletion 的可选 UnsafeOnCompleted 方法)之外,awaiter 还必须提供 IsCompleted 布尔属性以及无参数的 GetResult 方法。如果可等待对象表示返回 void 的操作,则 GetResult 返回 void;如果可等待对象表示返回 TResult 的操作,则返回 TResult。

可以等待任何遵循可等待模式的类型。有关实现自定义等待项的几种方法的讨论,请参阅等待任何内容;您还可以实现为非常特定的情况定制的可等待对象:有关某些示例,请参阅异步方法等待套接字操作中的高级 APM 消耗

哪里不能用“await”?

你不能使用等待:

是“等待任务”;和“task.Wait()”一样吗?

不。

“task.Wait()”是一个同步的、潜在的阻塞调用:它不会返回到 Wait() 的调用者,直到任务进入最终状态,这意味着它在 RanToCompletion、Faulted 或 Canceled 状态下完成。相反,“等待任务;” 告诉编译器在标记为“async”的方法中插入一个潜在的暂停/恢复点,这样如果任务在等待时还没有完成,异步方法应该返回到它的调用者,并且它的执行应该在且仅在当等待的任务完成时。在“等待任务”时使用“task.Wait()”;本来更合适会导致应用程序无响应和死锁;请参阅等待、UI 和死锁!天啊!.

在使用“async”和“await”时,还有一些其他潜在的陷阱需要注意。有关一些示例,请参阅:

“task.Result”和“task.GetAwaiter().GetResult()”之间在功能上有区别吗?

是的,但前提是任务未成功完成。如果任务以 RanToCompletion 状态结束,则这些是完全等效的语句。但是,如果任务以 Faulted 或 Canceled 状态结束,前者将传播包装在 AggregateException 中的一个或多个异常,而后者将直接传播异常(如果任务中有多个异常,它将传播只需传播其中之一)。有关为何存在这种差异的背景信息,请参阅.NET 4.5 中的任务异常处理

“await”与当前的 SynchronizationContext 有什么关系?

这完全取决于正在等待的类型。对于给定的等待,编译器生成的代码最终会调用等待者的 OnCompleted 方法,并传入要执行的延续委托。编译器生成的代码对 SynchronizationContext 一无所知,并且仅依赖于等待对象的 OnCompleted 方法在等待操作完成时调用提供的回调。然后,是 OnCompleted 方法负责确保在“正确的地方”调用委托,其中“正确的地方”完全由等待者决定。

等待任务的默认行为(分别由 Task 和 Task<TResult> 的 GetAwaiter 方法返回的 TaskAwaiter 和 TaskAwaiter<TResult> 类型实现)是在挂起之前捕获当前 SynchronizationContext,然后在等待的任务完成时捕获,如果当前的 SynchronizationContext 已被捕获,则将延续委托的调用发布回该 SynchronizationContext。因此,例如,如果您使用“await task;” 在应用程序的 UI 线程上,调用 OnCompleted 时将看到一个非空的当前 SynchronizationContext,当任务完成时,它将使用该 UI 的 SynchronizationContext 将继续委托的调用编组回 UI 线程。

如果在等待任务时没有当前的 SynchronizationContext,那么系统将检查是否有当前的 TaskScheduler,如果有,则将继续安排到任务完成时的状态。

如果没有这样的上下文或调度程序来强制继续执行,或者如果您执行“await task.ConfigureAwait(false)”而不仅仅是“await task;”,则继续将不会被强制返回到原始上下文,并将被允许在系统认为合适的任何地方运行。这通常意味着要么在等待任务完成的地方同步运行延续,要么在 ThreadPool 上运行延续。

我可以在控制台应用程序中使用“await”吗?

当然。但是,您不能在 Main 方法中使用“await”,因为入口点不能被标记为异步。相反,您可以在控制台应用程序的其他方法中使用“await”,然后如果您从 Main 调用这些方法,您可以同步等待(而不是异步等待)它们完成,例如

public static void Main()
{
    FooAsync().Wait();
}

私有静态异步任务 FooAsync()
{
    await Task.Delay(1000);
    Console.WriteLine(“第一次延迟完成”);
    等待 Task.Delay(1000);
}

您还可以使用自定义 SynchronizationContext 或 TaskScheduler 来实现类似的功能。有关更多信息,请参阅:

我可以将“await”与其他异步模式(例如异步编程模型 (APM) 模式和基于事件的异步模式 (EAP))一起使用吗?

当然。您可以为异步操作实现自定义等待,也可以将现有异步操作转换为已经等待的操作,例如 Task 或 Task<TResult>。这里有些例子:

async/await 生成的代码是否会导致高效的异步执行?

大多数情况下,是的,因为已经做了大量工作来优化编译器生成的代码以及生成的代码所依赖的 .NET Framework 方法。有关更多信息,包括最小化使用任务和 async/await 的开销的最佳实践,请参阅:




本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2024年4月    »
1234567
891011121314
15161718192021
22232425262728
2930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864