【问题标题】:async/await. Where is continuation of awaitable part of method performed?异步/等待。方法的可等待部分的延续在哪里执行?
【发布时间】:2015-09-20 02:06:53
【问题描述】:

我真的很好奇 async/await 如何使您的程序不被停止。 我真的很喜欢the way how Stephen Cleary explains async/await“我喜欢把“await”想象成一个“异步等待”。也就是说,async 方法会暂停,直到 awaitable 完成(所以它等待),但实际线程没有被阻塞(所以它是异步的)。”

我读过 async 方法会同步工作,直到编译器遇到 await 关键字。好吧。 如果编译器无法确定可等待,则编译器将等待和让出控制排队到调用方法AccessTheWebAsync 的方法。 好的。 在调用者(本例中的事件处理程序)内部,处理模式继续进行。在等待该结果之前,调用者可能会执行不依赖于来自AccessTheWebAsync 的结果的其他工作,或者调用者可能会立即等待。事件处理程序正在等待AccessTheWebAsync,而AccessTheWebAsync 正在等待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 在哪里执行可等待代码(在我们的示例中下载一个网站)导致控制权交给我们程序的下一行代码,而程序只询问Task&lt;string&gt; getStringTask 的结果?我们知道没有新线程,没有线程池不被使用。

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

【问题讨论】:

  • 查看等待的不是编译器。这是编译器编译的代码。

标签: c# asynchronous async-await task-parallel-library .net-4.5


【解决方案1】:

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

如果操作是真正异步的,那么就没有代码可以“执行”。你可以认为这一切都是通过回调处理的; HTTP 请求被发送(同步),然后HttpClient 注册一个回调,该回调将完成Task&lt;string&gt;。下载完成后,将调用回调,完成任务。它比这更复杂一些,但这是一般的想法。

我有一篇关于how asynchronous operations can be threadless 的博客文章更详细地介绍了。

我的愚蠢假设是否正确,即 CLR 只是在一个线程范围内相互切换当前可执行代码和方法的可等待部分?

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

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

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

    也就是说,他们只是想告诉您,如果您有一个方法 void Foo() { Console.WriteLine(); },将其更改为 async Task Foo() { Console.WriteLine(); } 不会突然导致对 Foo(); 的调用表现出任何不同 - 它仍然会同步执行。

    【讨论】:

    • 因此,如果等待代码尝试在 UI 线程上运行,它可以暂停 UI。但是,如果使用异步代码,则不会发生 UI 停止。
    • @StepUp 异步调用不在 UI 线程上运行。延续可以(如果需要,例如使用 HTTP 调用的结果更新文本框)。从你的问题标题来看,这就是你要问的正确 - continuation 在哪里运行?
    • 我真的很累,延续代码和异步代码有什么区别?如果等待代码无法立即返回结果,则将执行延续和异步代码的任务
    • @StepUp 在异步操作之后继续。它是await 之后的代码。在您的情况下,它是return urlContents.Length;。当然,它需要一个线程来运行,它通常是一个ThreadPool 线程。但是实际的异步操作是不需要的。
    【解决方案3】:

    如果 “等待代码” 是指实际的异步操作,那么您需要意识到 它在 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,它将在间隔后执行回调并完成任务。 System.Threading.Timer 不需要线程,它使用系统时钟。所以你有“等待代码”,它“执行”了 30 秒,但在那段时间里实际上什么都没有发生。操作已开始,将在 30 秒后完成。

    【讨论】:

    • 谢谢,但是如果代码等待 Task 结果在哪里执行,例如斐波那契数呢?这是一个需要 CPU 活动的操作,而不仅仅是等待下载结果。
    • “它在 CPU 之外执行,因此不需要线程,也不需要运行代码。” - 对于异步操作,这通常是正确的,但对于延续来说,不是正确的,我认为这是被问到的。延续在 CPU 上运行并且在线程上运行。
    • @Joren 我不认为这是被问到的。
    • @StepUp CPU 绑定操作不是自然异步的。它可以被卸载到 ThreadPool 线程(即使用 Task.Run)并被视为已被处理,但这不是 async-await 的一般情况。
    • @StepUp 在这种情况下,操作确实需要一个线程,即ThreadPool线程
    猜你喜欢
    • 2021-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-15
    • 2014-08-17
    相关资源
    最近更新 更多