async/await。方法的可等待部分的延续在哪里执行?

async/await. Where is continuation of awaitable part of method performed?

我很好奇async/await是如何让你的程序不被暂停的。 我真的很喜欢 the way how Stephen Cleary explains async/await: "I like to think of "await" 作为 "asynchronous wait"。也就是说,异步方法暂停直到等待完成(所以它等待),但是实际线程没有被阻塞(所以它是异步的)。"

我读到 async 方法会同步工作,直到编译器遇到 await 关键字。出色地。 如果编译器无法找出等待对象,则编译器会将等待对象排队并将控制权交给调用方法的方法 AccessTheWebAsync OK。 在调用者内部(本例中的事件处理程序),处理模式继续进行。在等待该结果之前,调用者可能会执行不依赖于 AccessTheWebAsync 结果的其他工作,或者调用者可能会立即等待。事件处理程序正在等待 AccessTheWebAsyncAccessTheWebAsync 正在等待 GetStringAsync。让我们看看 an msdn example:

async Task<int> AccessTheWebAsync()
{ 
    // You need to add a reference to System.Net.Http to declare client.
    HttpClient client = new HttpClient();

    // GetStringAsync returns a Task<string>. That means that when you await the 
    // task you'll get a string (urlContents).
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

    // You can do work here that doesn't rely on the string from GetStringAsync.
    DoIndependentWork();

    // The await operator suspends AccessTheWebAsync. 
    //  - AccessTheWebAsync can't continue until getStringTask is complete. 
    //  - Meanwhile, control returns to the caller of AccessTheWebAsync. 
    //  - Control resumes here when getStringTask is complete.  
    //  - The await operator then retrieves the string result from getStringTask. 
    string urlContents = await getStringTask;

    // The return statement specifies an integer result. 
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value. 
    return urlContents.Length;
}

Another article from msdn blog 表示 async/await 不会创建新线程或使用线程池 中的其他线程。好的。

我的问题:

  1. async/await 在哪里执行可等待的代码(在我们下载网站的示例中)导致控制让渡到我们程序的下一行代码,程序只询问 [=17 的结果=]?我们知道没有新线程,没有线程池是用不到的。

  2. 我愚蠢的假设是否正确,即 CLR 只是在一个线程的范围内相互切换当前的可执行代码和方法的可等待部分?但是改变加数的顺序不会改变总和,UI 可能会被阻塞一段不明显的时间。

一般来说,延续(await 之后的方法部分)可以 运行 任何地方。实际上,它倾向于在 UI 线程(例如在 Windows 应用程序中)或线程池(例如在 ASP .NET 服务器中)上 运行。在某些情况下,它还可以 运行 在调用者线程上同步......实际上这取决于你调用的 API 类型以及正在使用的同步上下文。

您链接的博客文章没有说延续不是运行在线程池线程上,它只是说将方法标记为async 不会神奇地导致在单独的线程或线程池上调用方法 运行。

也就是说,他们只是想告诉您,如果您有方法 void Foo() { Console.WriteLine(); },将其更改为 async Task Foo() { Console.WriteLine(); } 不会突然导致调用 Foo();行为 完全不同 – 它仍然会同步执行。

如果 "awaitable code" 是指实际的异步操作,那么您需要意识到 它 "executes" 在 CPU 所以不需要线程,也没有代码到 运行.

例如,当您下载网页时,大部分操作发生在您的服务器从网络服务器发送和接收数据时。发生这种情况时没有可执行的代码。这就是在等待 Task 获得实际结果之前,您可以 "take over" 线程并执行其他操作(其他 CPU 操作)的原因。

所以对于你的问题:

  1. 它 "executes" 在 CPU 之外(所以它并没有真正执行)。这可能意味着网络驱动程序、远程服务器等(主要是 I/O)。

  2. 没有。真正的异步操作不需要由 CLR 执行。它们只是在未来开始和完成。


一个简单的示例是 Task.Delay,它创建一个在间隔后完成的任务:

var delay = Task.Delay(TimeSpan.FromSeconds(30));
// do stuff
await delay;

Task.Delay 内部创建并设置了一个 System.Threading.Timer 会在间隔后执行回调并完成任务。 System.Threading.Timer不需要线程,它使用系统时钟。所以你有 "awaitable code" 那 "executes" 30 秒,但那段时间实际上没有发生任何事情。操作已开始,将在 30 秒后完成。

Where does async/await execute awaitable code(in our example downloading a web site) cause control yields to the next row of code of our program and program just asks result of Task getStringTask? We know that no new threads, no thread pool are not used.

如果操作确实是异步的,那么 "execute" 就没有代码了。你可以认为它都是通过回调处理的;发送 HTTP 请求(同步),然后 HttpClient 注册一个将完成 Task<string> 的回调。下载完成后,将调用回调,完成任务。它比这复杂一点,但这是一般的想法。

我有一个博客 post,其中详细介绍了 how asynchronous operations can be threadless

Am I right in my silly assumption that CLR just switches the current executable code and awaitable part of the method between each other in scope of one thread?

这是一个部分正确的心理模型,但它是不完整的。一方面,当 async 方法恢复时,它的(以前的)调用堆栈不会随之恢复。所以 async/awaitfibers or co-routines 非常不同,尽管它们可以用来完成类似的事情。

与其将 await 视为 "switch to other code",不如将其视为 "return an incomplete task"。如果调用方法 调用 await,那么它也是 return 一个未完成的任务,等等。最终,你要么 return 一个未完成的任务框架任务(例如,ASP.NET MVC/WebAPI/SignalR,或单元测试运行器);或者您将有一个 async void 方法(例如,UI 事件处理程序)。

当操作正在进行时,您最终会得到 "stack" 个任务对象。不是真正的堆栈,只是一个依赖树。每个 async 方法都由一个任务实例表示,它们都在等待异步操作完成。

Where is continuation of awaitable part of method performed?

等待任务时,await 将 - 默认情况下 - 在捕获的上下文中恢复其 async 方法。这个上下文是 SynchronizationContext.Current 除非它是 null,在这种情况下它是 TaskScheduler.Current。实际上,这意味着 UI 线程上的 async 方法 运行 将在该 UI 线程上恢复;处理 ASP.NET 请求的 async 方法将继续处理相同的 ASP.NET 请求(可能在不同的线程上);在大多数其他情况下,async 方法将在线程池线程上恢复。

在您问题的示例代码中,GetStringAsync 将 return 一项未完成的任务。下载完成后,该任务将完成。因此,当 AccessTheWebAsync 在该下载任务上调用 await 时,(假设下载尚未完成)它将捕获其当前上下文,然后 return 来自 [=28= 的未完成任务].

当下载任务完成时,AccessTheWebAsync 的继续将被调度到该上下文(UI 线程,ASP.NET 请求,线程池,...),并且它将在该上下文中执行时提取结果的 Length 。当 AccessTheWebAsync 方法 returns 时,它设置先前 return 从 AccessTheWebAsync 编辑的任务的结果。这又会恢复下一个方法等