C# 5.0 vNext - 新的异步模式






4.82/5 (93投票s)
C# 5.0 CTP 最近发布,本文将特别介绍我对此次发布的理解以及一些示例应用程序
目录
- 引言
- 历史
- 什么是 TAP (任务异步模式)?
- 当前异步模式
- 异步模式的先决条件
- 什么是 async 和 await?
- 它是如何工作的?
- 一个隐藏的真相
- 异常处理如何?
- 一次性调用所有
- 任务取消
- 与 CPU 一起运行
- 需要记住的事情
- 深入了解事实
- 调试 API
- 参考文献
- 历史
- 结论
引言
大家好!最近社区热议,随着 C# vNext 在 .NET 语言世界(更准确地说是 C# 和 VB.NET)中推出,是时候让大家了解并理解这些功能在当前场景中是如何工作的了。确实,很多人都在写关于它的文章,但相信我,我没有看到任何一篇能在一个地方给出完整示例的。因此,我想我应该尝试一下。在这篇文章中,我将尝试涵盖与异步编程模型相关的大部分内容,并让您有机会理解这个概念并提供反馈。
旁注: 我的一篇文章正在进行月度调查,您可能也会喜欢这篇文章。它也出现在这里(另一篇)
历史
如果我从头开始谈论 C#,那将是一条漫长的路。但如果我在这里给您介绍 C# 作为一种语言的整个演变过程,然后再深入了解 C# 的最新调整,那将是很有趣的。在 PDC 10 上,Anders 指出了 C# 语言的整个演变,让我们先来看看它。

C# 基本上不断地被新的概念丰富,因此它逐渐成为行业中最好的语言,但它确实包含了每个现有语言功能的向后兼容性。在上面的图中,您可以看到,C# 早在 2001 年就被引入作为一种语言,当时它首次作为真正的托管代码语言推出。最初的 C# 语言很棒,但缺少很多东西。后来,C# 2.0 推出。它引入了一个很棒的新功能泛型,这是以前从未引入过的。C# 3.0 实际上是为了 LINQ,一种真正的语言集成查询语法,您可以使用简单的 lambda 或 LINQ 表达式查询现有的 .NET 数据结构,而无需手动遍历列表。最后,在 .NET 4.0 中引入了任务并行库,它通过为每个正在进行的操作提供任务,让您创建更具响应性的应用程序。
什么是任务 (TASK)?
如果您使用过 .NET 的并行扩展,您可能已经知道 Task 被引入来表示未来。它表示一个正在进行的操作,将在未来产生一个输出。Task 对象有一些您可能会关注的特性。
- 任务调度
- 在子任务与其父任务之间建立父子关系
- 实现协同取消
- 无需外部等待句柄即可发出等待信号
- 使用
ContinueWith
附加“延续”任务
所以 task 是任务并行库的基本单元,它封装了我们调用 Thread
所需的一切,并允许程序模块并行运行。
C# 5.0 的推出有一个主要目标,那就是以一种方式塑造语言,使程序员能够像同步编程那样编写逻辑,同时保持程序的原始流程。C# 已经有线程来处理异步编程。使用 C# 2.0 编写异步风格的编程非常容易,您可以创建一个 Delegate
并调用其自己的 BeginInvoke
和 EndInvoke
实现,并放入一个回调,该回调将在执行完成后调用。但基本上发生的情况是,随着您越来越多地引入异步模式,您的代码会变得越来越复杂。您需要为每个调用设置回调,以便当调用完成时,操作最终会调用回调,并且您的其余逻辑才会执行。因此,随着代码中异步性程度的提高(我们经常需要),您的代码将变得越来越复杂,并且对其他人来说难以理解。C# 5.0 保持了编程模型的完整性,但让您像编写同步编程风格那样编写应用程序,具有相同的逻辑流程和结构,但对代码进行了一些调整,并且工作将异步进行。
什么是 TAP (任务异步模式)?
因此,如果您继续使用异步模式,从并行性的角度来看,我们将新模式称为任务异步模式。这是 C# 1.0 引入线程之后,首次尝试稍微改变语言,使其更容易编写真正响应式的应用程序。响应式应用程序意味着,我们可以在一个线程上完成更多工作,而不是阻塞线程以实现 UI 响应。
异步和并发的区别
我们大多数人对异步和并发的概念感到困惑。嗯,我想说您的想法与现在不同。让我们以并行性为例。如果您考虑并发,我们正在考虑处理 CPU 周期。例如,您想为应用程序进行大量计算。在这种情况下,您希望并行进行计算。那么您怎么想呢?您需要多个线程并行运行,最终获得输出。因此,如果您考虑应用程序运行中的某个时间点,实际上有多个 CPU 忙于您的计算,并最大程度地利用 CPU,同时并发访问您的资源。这就是我们所说的并发。
另一方面,异步是并发的超集。因此,它既包括并发,也包括其他不受 CPU 限制的异步调用。假设您正在硬盘上保存一个大文件,或者您想从服务器下载一些数据。这些事情确实会共享您的 CPU 时间片。但如果可以以异步模式完成,它也将包含在异步中。
总结一下,异步是一种模式,在调用时立即交出控制权,但等待回调稍后出现。并发则是在 CPU 密集型应用程序中并行执行。
它真的包含用于异步的线程吗?
从字面上讲,不……不是。在当前场景中,如果您需要创建一个响应式应用程序,您总是会想到从线程池中创建一个新线程,调用资源(这最终会暂时阻塞线程),最后当设备说它准备好了时获取结果。但这总是说得通吗?不。假设您正在下载一部电影,这是网络绑定的。因此,在这种情况下基本上没有 CPU 的参与。所以根据 Microsoft 的说法,您的 UI 线程实际上是创建消息并将其抛给网络设备来完成这项工作,并立即返回而无需获取结果。网络设备将相应地执行其任务(下载 URL),最后使用 SynchronizationContext
适当地找到 UI 线程并返回结果。因此,当网络设备完成其工作时,它可以与相应的线程通信并调用回调。这听起来不错,不是吗?
是的,比如说您需要并行进行 10 次网络调用。在以前的方法中,您会创建 10 个线程,每个线程都会调用网络 API 并阻塞自身,直到获得响应。因此,即使您没有使用 CPU 的一个时间片,所有 ThreadPool
线程都会耗尽。您可能会想,增加 ThreadPool 线程大小如何?嗯,您可能可以,但如果所有网络资源都在一段时间后可用,这会增加额外的压力。因此,异步模式可以在这方面帮助您。这样,您将从同一线程异步调用每个资源,并等待回调的出现。
当前异步模式
同步块
在我介绍我们引入的内容之前,了解现有内容非常有趣,这样您就可以比较两种方法。假设您想从服务器下载一些链接,目前我正在同步进行,我可能会使用
const string Feed = "https://codeproject.org.cn/webservices/articlerss.aspx?cat={0}"; private void btnSync_Click(object sender, RoutedEventArgs e) { this.SynchronousCallServer(); } public void SynchronousCallServer() { WebClient client = new WebClient(); StringBuilder builder = new StringBuilder(); for (int i = 2; i <= 10; i++) { this.tbStatus.Text = string.Format("Calling Server [{0}]..... ", i); string currentCall = string.Format(Feed, i); string rss = client.DownloadString(new Uri(currentCall)); builder.Append(rss); } MessageBox.Show( string.Format("Downloaded Successfully!!! Total Size : {0} chars.", builder.Length)); }
在上述方法中,我直接使用 WebClient
调用了一个 Web 服务器 URL,并下载了整个字符串,然后将其附加到 builder 对象中。最后,当所有链接都下载完毕(按顺序)后,我们向用户显示一个 MessageBox
。您还应该注意到,我们在下载 URL 的同时也在更新状态消息。现在让我们运行应用程序并调用该方法。

在上面的图中,您可以看到应用程序停止响应,因为当调用 DownloadString
时,UI 线程被阻塞。您还应该注意到,屏幕不会随状态消息更新,因为我们只有在所有字符串下载完成后才能看到最终状态消息。
异步块(旧方法)
现在,这不是一个好的 UI 设计。是吗?我说不。所以让我们将它异步化,以便像我们正在做的那样按顺序调用下载字符串。那么代码应该看起来有什么不同呢?
const string Feed = "https://codeproject.org.cn/webservices/articlerss.aspx?cat={0}"; private void btnaSyncPrev_Click(object sender, RoutedEventArgs e) { StringBuilder builder = new StringBuilder(); this.AsynchronousCallServerTraditional(builder, 2); } public void AsynchronousCallServerTraditional(StringBuilder builder, int i) { if (i > 10) { MessageBox.Show( string.Format("Downloaded Successfully!!! Total Size : {0} chars.", builder.Length)); return; } this.tbStatus.Text = string.Format("Calling Server [{0}]..... ", i); WebClient client = new WebClient(); client.DownloadStringCompleted += (o,e) => { builder.Append(e.Result); this.AsynchronousCallServerTraditional(builder, i + 1); }; string currentCall = string.Format(Feed, i); client.DownloadStringAsync(new Uri(currentCall), null); }
天哪!这太棒了。代码看起来完全不同了。首先,WebClient
的 DownloadStringAsync
不会返回字符串,因为控制权会立即返回,因此要获取结果,我们需要为 DownloadStringCompleted
添加一个 EventHandler
。由于 EventHandler
实际上是一个完全不同的方法,我们需要每次将 StringBuilder
对象发送到回调方法,并且最终回调会递归地一次又一次地调用相同的方法来创建整个数据结构。所以基本上,对相同方法的异步调用看起来完全不同。

异步块(新方法)
随着 C# vNext 的推出,异步模式被简化得天翻地覆。正如 Anders Hejlsberg 在 PDC 10 这里展示的那样,引入了两个新关键字“async”和“await”,从语言角度完全简化了异步(您也可以阅读我在 PDC 之后发布的我的帖子)。在我们深入探讨这些之前,让我看看这种方法中的相同代码。
const string Feed = "https://codeproject.org.cn/webservices/articlerss.aspx?cat={0}"; public async Task AsynchronousCallServerMordernAsync() { WebClient client = new WebClient(); StringBuilder builder = new StringBuilder(); for (int i = 2; i <= 10; i++) { this.tbStatus.Text = string.Format("Calling Server [{0}]..... ", i); string currentCall = string.Format(Feed, i); string rss = await client.DownloadStringTaskAsync(new Uri(currentCall)); builder.Append(rss); } MessageBox.Show( string.Format("Downloaded Successfully!!! Total Size : {0} chars.", builder.Length)); }
所以代码看起来完全一样,只是多了两个关键字 "async" 和 "await"。它工作吗?是的,当您尝试运行代码时,它的工作方式与同步方式相同。您已经注意到我使用了 DownloadStringTaskAsync
,它实际上是 DownloadString
的异步实现,采用相同的方法。它是一个扩展方法,只有在您安装 async CTP 后才能使用。
异步模式的先决条件
异步模式仍处于 CTP 阶段,因此您需要在机器上安装一些位,这将更新您的 Visual Studio 2010 以允许使用这两个关键字。
在您的机器上安装这两个之后,将 AsyncCTPLibrary
引用到您的应用程序中,该库可以在 %MyDocument%\Microsoft Visual Studio Async CTP\Samples\AsyncCtpLibrary.dll
中找到。
什么是 async 和 await?
根据 PDC,我们可以看到 async 是方法的一个新修饰符,它允许方法在被调用后立即返回。所以当一个方法被调用时,控制权会立即交出,直到它遇到第一个 await。所以 await 是一个上下文关键字,可以放在任何 Task
(或任何实现了 GetAwaiter
的对象)对象之前,它允许您在方法执行时立即返回控制权。一旦方法执行完毕,调用者会找到相同的块并调用 GoTo 语句回到我们离开的位置,并注册我们编写的其余代码。所有这些调整都由编译器自动完成,不需要用户干预。
它是如何工作的?
嗯,编译器擅长状态机。如果您考虑 C# IEnumeramble
,编译器实际上会为每个您调用的 yield 语句在内部构建一个状态机。所以当您编写的逻辑对应于一个包含 yield 语句的方法时,整个状态机逻辑由编译器准备,以便在某个 yield 上,它会根据工作流程自动确定可能的下一个 yield。C# 异步是基于相同的方法构建的。编译器对您的程序进行了适当的修改,以构建该块的完整状态机。因此,当您为某个块编写 await 时,它实际上在调用方法后交出控制权,并返回一个表示正在进行的方法的 Task
对象。如果正在进行的方法返回某些内容,则它作为 Task<T>
返回。让我们看下面的图。

代码分两个阶段运行。首先,它调用 AsynchronousCallServerMordern
方法,直到它找到一个 await,该 await 会在将其余方法注册到其状态机工作流步骤后立即返回一个 Task 对象。但在返回给调用者后,它再次调用一个 await,这会将控制权返回给 UI(async
方法中的 void 返回类型意味着调用即忘记)。因此,UI 将立即可用。在对 DownloadStringTaskAsync
的调用完成后,程序会占用一个时间片来运行现有代码,直到它找到下一个 await,并持续进行,直到所有链接都成功下载。最后,控制权完全释放给 UI。
一个隐藏的真相(阅读了解更多)
当您思考这种情况时,您可能会想,在执行整个方法实例之前返回控制权怎么可能。由于我没有告诉您这方面的全部事实,这种困惑可能会发生。让我尝试进一步澄清这一点。
所以,当你创建一个异步方法时,你实际上是在创建一个方法的新实例。方法通常是用来运行的。但如果你从实际角度考虑方法,是实例在运行。.NET 实际上创建了一个状态机对象,它包含了方法体的不同步骤。如果你已经使用过状态机,你一定知道,状态机可以轻松地跟踪流程的当前状态。所以方法实例不过是一个状态机的实例,它包含了参数、局部变量以及方法当前的运行状态。因此,当你遇到 await 语句时,它实际上将状态机对象保存为一个变量,当 awaiter 得到响应时,它会再次恢复该变量。
例如,我写下以下代码
Console.WriteLine("Before await"); await TaskEx.Delay(1000); Console.WriteLine("After await");
现在,当您编译程序集时,它实际上会创建一个带有两个方法(每个状态一个)的状态机对象。
- 它接受第一个委托并将其与
Console.WriteLine("Before await"); await TaskEx.Delay(1000);
比如,它将其命名为状态 1。
- 它将剩余部分移到一个新方法中。
Console.WriteLine("After await");
所以基本上这里没有单个方法体,但是编译器创建了 2 个方法(我将在定义 TaskAwaiter
后详细阐明这些),其中第一个方法首先被调用,对象将当前状态存储为 State1,等待之后,当 Task Placeholder 得到适当的结果时,它会调用 Next State 并完成。
异常处理如何?
令人惊讶的是,异常处理与异步的同步调用方式相同。以前在处理异步代码时,例如 WebClient.DownloadStringAsync
,我们需要手动检查每次 DownloadComplete
调用中 e.Error
是否有任何值。但对于异步模式,您可以使用自己的 Try/Catch 块来包装异步代码,错误将自动抛出并在块中捕获。因此,如果您想为代码添加异常处理,您可以这样编写
public async Task AsynchronousCallServerMordernAsync() { WebClient client = new WebClient(); StringBuilder builder = new StringBuilder(); for (int i = 2; i <= 10; i++) { try { this.tbStatus.Text = string.Format("Calling Server [{0}]..... ", i); string currentCall = string.Format(Feed, i); string rss = await client.DownloadStringTaskAsync(new Uri(currentCall)); builder.Append(rss); } catch(Exception ex) { this.tbStatus.Text = string.Format("Error Occurred -- {0} for call :{1}, Trying next", ex.Message, i); } MessageBox.Show( string.Format("Downloaded Successfully!!! Total Size : {0} chars.", builder.Length)); }
酷。是的,确实如此。异步模式无需进行任何修改即可处理异常。
一次性调用所有
既然一切都已完成,您可能会想,是否不可能并行而不是顺序地调用每个语句,这样我们就可以节省一些时间。是的,您说得对。完全可以一次性处理多个任务,然后一次性等待所有任务。为此,让我修改一下代码。
private async void btnaSyncPresParallel_Click(object sender, RoutedEventArgs e) { await this.AsynchronousCallServerMordernParallelAsync(); } public async Task AsynchronousCallServerMordernParallelAsync() { List> lstTasks = new List >(); StringBuilder builder = new StringBuilder(); for (int i = 2; i <= 10; i++) { using (WebClient client = new WebClient()) { try { this.tbStatus.Text = string.Format("Calling Server [{0}]..... ", i); string currentCall = string.Format(Feed, i); Task task = client.DownloadStringTaskAsync(new Uri(currentCall)); lstTasks.Add(task); } catch (Exception ex) { this.tbStatus.Text = string.Format("Error Occurred -- {0} for call :{1}, Trying next", ex.Message, i); } } } try { string[] rss = await TaskEx.WhenAll (lstTasks); foreach(string s in rss) builder.Append(s); } catch {} MessageBox.Show( string.Format("Downloaded Successfully!!! Total Size : {0} chars.", builder.Length)); }
所以基本上,我们需要做的就是在循环内部创建 WebClient 对象,并且还需要删除 DownloadStringTaskAsync
调用的 await。正如我之前告诉过您的,await 会立即将控制权返回给调用者,我们不需要它立即返回。相反,我们创建一个所有任务的列表,最后使用 TaskEx.WhenAll
将所有任务聚合到一个任务中,并对其调用 await。因此,TaskEx.WhenAll
将返回一个字符串数组,您可以在所有任务完成时使用它。天哪,对它的调用会立即返回。
任务取消
代码的另一个重要部分是取消现有正在进行的任务。为此,异步模式引入了一个名为 CancellationTokenSource
的新对象。您可以使用它来取消正在进行的操作。让我们看一个示例应用程序来演示取消。

在此应用程序中,我们有两个按钮,其中一个调用 CodeProject RSS 提要,而取消按钮则调用操作的取消。让我们快速了解一下我是如何构建该应用程序的。
string url = "https://codeproject.org.cn/WebServices/ArticleRSS.aspx"; CancellationTokenSource cancelToken; private async void btnSearch_Click(object sender, RoutedEventArgs e) { var result = await LoadArticleAsync(); this.LoadArticleList(result); await TaskEx.Delay(5000); if (cancelToken != null) { cancelToken.Cancel(); tbstatus.Text = "Timeout"; } } private void btnCancel_Click(object sender, RoutedEventArgs e) { if (cancelToken != null) { cancelToken.Cancel(); tbstatus.Text = "Cancelled"; } } void LoadArticleList(string result) { var articles = from article in XDocument.Parse(result).Descendants("item") let subject = article.Element("subject").Value let title = article.Element("title").Value let description = article.Element("description").Value let url = article.Element("link").Value let author = article.Element("author").Value let publishdate = article.Element("pubDate").Value select new Article { Subject = subject, Title = title, Description = description, PublishDate = publishdate, Author = author, Url = url }; ICollectionView icv = CollectionViewSource.GetDefaultView(articles); icv.GroupDescriptions.Add(new PropertyGroupDescription("Subject")); this.lstArticles.DataContext = icv; } async TaskLoadArticleAsync() { cancelToken = new CancellationTokenSource(); tbstatus.Text = ""; try { tbstatus.Text = "Searching... "; var client = new WebClient(); var taskRequest = await client.DownloadStringTaskAsync(this.url); tbstatus.Text = "Task Finished ... "; return taskRequest; } catch (TaskCanceledException) { return null; } finally { cancelToken = null; } }
所以基本上,我使用了一个 WPF 应用程序(为了简化,我没有使用 MVVM),其中我调用了一个 web DownloadStringTaskAsync
调用。对于 TaskCancellation
,您唯一需要做的就是在您想要取消的块中创建 CancellationTokenSource
的实例。在上面的代码中,我在 LoadArticleAsync
方法中创建了该对象,因此无论何时我调用 cancelToken.Cancel
,对该调用的 await
都将被释放。您甚至可以进行诸如 cancelToken.CancelAfter(2000)
的调用,以在 2 秒后取消操作。
与 CPU 一起运行
最后但同样重要的是,如果您真的想使用新 Thread
来调用您的方法怎么办?使用 async
模式,这比以往任何时候都更容易实现。只需看一下代码。
private async void btnGoCPUBound_Click(object sender, RoutedEventArgs e) { await new SynchronizationContext().SwitchTo(); //Switch to net Thread from ThreadPool long result = this.DoCpuIntensiveWork(); //Very CPU intensive await Application.Current.Dispatcher.SwitchTo(); MessageBox.Show(string.Format("Largest Prime number : {0}", result)); } public long DoCpuIntensiveWork() { long i = 2, j, rem, result = 0; while (i <= long.MaxValue) { for (j = 2; j < i; j++) { rem = i % j; if (rem == 0) break; } if (i == j) result = i; i++; } return result; }
在上述情况下,我们使用 SynchronizationContext.SwitchTo
来获取一个 YieldAwaitable
,它从 ThreadPool
创建一个新线程并返回一个实现 GetAwaitable
的对象。因此,您可以在其上等待。稍后,您可以使用 Dispatcher.SwitchTo
返回到原始线程。那么,这不是很简单吗?是的,现在在不同线程之间切换真的很有趣。
需要记住的事情
在这方面需要记住很多事情,让我们列出它们:
- 对于任何
async
块,至少有一个await
很重要,否则整个块将同步工作。 - 任何
async
方法都应以 Async 为后缀(作为规则),因此您的方法名称应类似于MyMethodAsync
,在其前面加上async
关键字。 - 任何异步方法都可以根据 await 方法发送的
Result
返回 void(调用即忘记)、Task
或Task<T>
。 - 编译器会进行调整,找到相同的调用者并调用
GoTo
语句,以从调用 await 的相同位置执行其余逻辑,而不是进行回调。 - 一切都由编译器通过状态机工作流进行管理。
- CPU 密集型调用可以拥有自己的线程,但 async 方法本身不会创建新线程并阻塞它以等待调用者到达。网络或 I/O 密集型调用不会在后台创建任何新线程。
- 任何需要 await 的对象都必须关联
GetAwaiter
(即使是扩展方法)。
这基本上是您必须知道的关于异步模式的内容。在后面的部分中,我将尝试深入探讨异步框架的更多方面。
深入了解事实
嗯,深入了解到底发生了什么总是好的。为了检查,我使用了 Reflector 并发现了一些有趣的事实。
正如我之前告诉您的,await 仅适用于实现 GetAwaiter
的对象。现在 Task 具有 GetAwaiter
方法,它返回另一个对象(称为 TaskAwaiter
),用于实际注册 await 模式。任何 awaiter 对象都应包含 BeginAwait
和 EndAwait
等基本方法。库中实现了相当多的可等待方法。现在,如果您查看 BeginAwait
和 EndAwait
,它基本上是在创建一些委托,类似于您在普通异步模式下可能做的事情。

BeginAwait 实际上调用了 TrySetContinuationforAwait
,它实际上将现有方法体分成两个独立的块,并将每个 await 语句的下一部分注册到 Task 的延续中,就像您正在做的那样。
Task1.ContinueWith(Task2);
其中 Task2 表示回调中要运行的代码的其余部分。因此,如果希望您的对象与异步模式一起工作,则必须实现 GetAwaiter
,该方法返回定义了 BeginAwait
和 EndAwait
的对象。令人惊讶的是,扩展方法目前也可以用于编写相同的内容,我很期待看到在原始版本中是否可能。
您自己的位如何?(仅限 CTP)
如前一节所示,我展示了如何构建一个等待器,但让我们看看编译器完成操作后我们自己的应用程序是什么样子。
为了演示这一点,让我创建一个最简单的、只包含一个 await 的应用程序,并尝试演示它的 IL。让我们看看代码:
public class AsyncCaller { public async void GetTaskAsync(Listtasks) { await TaskEx.WhenAny(tasks); } }
所以,我创建了一个类库并向其中添加了这个方法。它基本上等待列表中任何任务完成。现在,如果您查看程序集内部,它会是这样的:
public void GetTaskAsync(Listtasks) { d__0 d__ = new d__0(0); d__.<>4__this = this; d__.tasks = tasks; d__.MoveNextDelegate = new Action(d__.MoveNext); d__.$builder = VoidAsyncMethodBuilder.Create(); d__.MoveNext(); }
有一个编译器生成的类型,它将代码的两个部分分成两个任务,并允许它继续执行使用
VoidAsyncMethodBuilder.Create在
WhenAll
返回输出之后。[CompilerGenerated] private sealed class <GetTaskAsync>d__0 { // Fields private bool $__disposing; private bool $__doFinallyBodies; public VoidAsyncMethodBuilder $builder; private int <>1__state; public List<Task> <>3__tasks; public AsyncCaller <>4__this; private Task <1>t__$await1; private TaskAwaiter<Task> <a1>t__$await2; public Action MoveNextDelegate; public List<Task> tasks; // Methods [DebuggerHidden] public <GetTaskAsync>d__0(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] public void Dispose() { this.$__disposing = true; this.MoveNext(); this.<>1__state = -1; } public void MoveNext() { try { this.$__doFinallyBodies = true; if (this.<>1__state != 1) { if (this.<>1__state == -1) { return; } this.<a1>t__$await2 = TaskEx.WhenAny(this.tasks).GetAwaiter<Task>(); this.<>1__state = 1; this.$__doFinallyBodies = false; if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)) { return; } this.$__doFinallyBodies = true; } this.<>1__state = 0; this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); Task task1 = this.<1>t__$await1; this.<>1__state = -1; this.$builder.SetCompleted(); } catch (Exception) { this.<>1__state = -1; this.$builder.SetCompleted(); throw; } }
是的,CompilerGenerated
嵌套类型创建了必要的调整来隐藏对 BeginAwait
的原始调用。如果你看到 MoveNext
,你肯定会明白 TaskEx.WhenAny
是从那里被调用的,它获取了 Awaiter。然后 Awaiter 被注册到 MoveNextDelegate,它存储了程序的延续部分。一旦 MoveNext
完成了它的任务,它就会使用 builder.SetCompleted
进行通知,这最终会尝试调用可能的下一个委托(它不存在)。
隐藏的真相(续)...
现在,您已经知道方法在async
的情况下并非按照我们通常的方式运行,它实际上创建了一个状态机对象。VoidAsyncMethodBuilder.Create
实际上返回状态机对象,该对象在编译器生成的密封类中内部保存了方法的每个状态。创建的匿名类型将每个方法保存在公共属性中,例如对我来说,d__.MoveNextDelegate
在调用 TaskAwaiter.BeginAwait
之前被设置。因此,关键是,当我们最初调用方法时,我们实际上是在调用方法的第一部分,在它调用
BeginAwait
之后,它最终会将整个对象作为状态机保存在上下文中并返回控制权。现在当任务有结果时,它会自动从自身调用 EndAwaiter
,并且对象再次被考虑,它会查看对象当前处于什么状态,并相应地调用 MoveNext(这最终会调用方法的下一部分)。
您应该注意,这种创建大型计算机生成类型的情况不会出现在最终版本中,但我只是为了演示概念并更好地理解而展示了这一点。我也建议您深入研究以了解更多信息。
调试 API
目前调试方面没有太多内容,但 System.Runtime.CompilerServices.AsyncMethodBuilder
中仍然有一个名为 DebugInfo
的类。虽然该类是内部的,但您仍然可以调用静态方法 ActiveMethods
来获取一些可用于调试的数据,例如尚未完成的方法,或者更准确地说,上下文中具有中间状态机的方法将在此处列出。
ActiveMethod 实际上返回一个 List<DebugInfo>
,其中列出了:
- StartDateTime(开始日期时间)
- StartingThread(开始线程)
- StartingExecutionContext(开始执行上下文)
- 任务
当任何 Task 被 await 时,每次它都会构建一个状态机对象,该对象保存方法体的每个步骤。在此的初始化阶段,会调用 AsyncMethodBuilder。在构造函数中,创建了 DebugInfo。如果您在 reflector 中查看,您会看到:AsyncMethodBuilder
实际上会添加 Task
对象以在其内部创建一个 DebugInfo
,然后在 Task 完成时将其移除。DebugInfo
实际上只列出了 ActiveMethods,而其他方法无法从调试器中浏览。
这基本上处于非常初步的阶段,所以我认为在未来版本的异步框架中它会增加更多功能。
要查看这些信息,您可以在 Visual Studio 的“监视”窗口中添加变量,并快速查看它们。有关更多详细信息,您可以浏览我关于此的博客文章。
参考文献
历史
- 初稿:2010 年 11 月 14 日
- 第二版:修复了图片问题和一些拼写错误。
- 第三版:应一些读者的要求,我澄清了一些章节。
- 第四版:增加了异步的调试功能
结论
在新异步模式推出后,我一直在思考如何通过自己尝试一些应用程序来学习它。我做的一切都只是为了好玩。如果我犯了任何错误,请告诉我。也请对这种新模式提出您的反馈。
感谢阅读