async/await.其中是所执行方法的不适用部分的延续

本文关键字:方法 不适用部 延续 执行 await async | 更新日期: 2023-09-27 18:00:53

我真的很好奇async/await是如何使程序不被暂停的。我真的很喜欢Stephen Cleary解释async/await的方式:"我喜欢把"await"看作是"异步等待"。也就是说,async方法会暂停,直到awaitable完成(所以它会等待),但实际线程没有被阻塞(所以它是异步的)。"

我读过async方法同步工作,直到编译器遇到等待关键字。嗯如果compilator无法计算出awaitable,则compilator将awaitable排队并将控制权交给调用方法AccessTheWebAsync的方法好。在调用者(本例中的事件处理程序)内部,处理模式继续。调用者可能会在等待AccessTheWebAsync的结果之前做其他不依赖于该结果的工作,或者调用者可能会立即等待。事件处理程序正在等待AccessTheWebAsync,而AccessTheWebAsync正在等待GetStringAsync。让我们看一个msdn示例:

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;
}

msdn博客中的另一篇文章说async/await不会创建新线程或使用线程池中的其他线程。好的。

我的问题:

  1. async/await执行不可执行的代码(在我们下载网站的例子中)在哪里导致我们程序的下一行代码的控制让步,而程序只询问Task<string> getStringTask的结果?我们知道没有新的线程,没有线程池没有被使用。

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

async/await.其中是所执行方法的不适用部分的延续

async/await执行不可执行的代码(在我们下载网站的例子中)在哪里导致我们程序的下一行代码的控制让步,而程序只询问Task getStringTask的结果?我们知道没有新的线程,没有线程池没有被使用。

如果操作真的是异步的,那么就没有代码可以"执行"。你可以认为这一切都是通过回调来处理的;HTTP请求被发送(同步)并且然后CCD_ 7注册将完成CCD_。下载完成后,将调用回调,从而完成任务。它比这个复杂一点,但这是一般的想法。

我有一篇博客文章,详细介绍了异步操作是如何实现无线程化的。

我的愚蠢假设是正确的吗?CLR只是在一个线程的范围内切换当前可执行代码和方法的不可执行部分?

这是一个部分真实的心理模型,但它是不完整的。首先,当async方法恢复时,其(以前的)调用堆栈不会随之恢复。因此,async/await与光纤或协例程非常不同,尽管它们可以用来完成类似的事情。

与其把await看作是"切换到其他代码",不如把它看作是"返回一个未完成的任务"。如果调用方法调用await,那么它也会返回一个不完整的任务,等等。最终,您将向框架(例如,ASP.NET MVC/WebAPI/SignalR或单元测试运行程序)返回一个未完成的任务;或者您将有一个async void方法(例如UI事件处理程序)。

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

在何处继续进行方法的可回收部分?

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

在问题的示例代码中,GetStringAsync将返回一个未完成的任务。下载完成后,该任务将完成。因此,当AccessTheWebAsync在该下载任务上调用await时(假设下载尚未完成),它将捕获其当前上下文,然后从AccessTheWebAsync返回一个未完成的任务。

下载任务完成后,AccessTheWebAsync的延续将被安排到该上下文(UI线程、ASP.NET请求、线程池…),并且它将在该上下文中执行时提取结果的Length。当AccessTheWebAsync方法返回时,它设置先前从AccessTheWebAsync返回的任务的结果。这反过来又将恢复下一个方法等。

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

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

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

如果所说的"awaitable code"是指实际的异步操作,那么您需要意识到它在CPU之外"执行",因此不需要线程,也不需要运行代码

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

因此,对于您的问题:

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

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


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

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

Task.Delay在内部创建并设置一个System.Threading.Timer,该CCD_39将在间隔后执行回调并完成任务。System.Threading.Timer不需要线程,它使用系统时钟。因此,您有"可执行代码",它"执行"了30秒,但在这段时间内实际上什么都没有发生。操作已启动,并将在30秒内完成。