【问题标题】:Multiple Awaits in a single method单个方法中的多个等待
【发布时间】:2013-08-29 00:15:31
【问题描述】:

我有这样的方法:

public static async Task SaveAllAsync()
{
    foreach (var kvp in configurationFileMap)
    {
        using (XmlWriter xmlWriter = XmlWriter.Create(kvp.Value, XML_WRITER_SETTINGS))
        {
            FieldInfo[] allPublicFields = 
                           kvp.Key.GetFields(BindingFlags.Public | BindingFlags.Static);
            await xmlWriter.WriteStartDocumentAsync();
            foreach (FieldInfo fi in allPublicFields)
            {
                await xmlWriter.WriteStartElementAsync("some", "text", "here");
            }
            await xmlWriter.WriteEndDocumentAsync();
        }
    }
}

但是当有人打电话给SaveAllAsync() 时,我很难理解会发生什么。

我认为会发生的事情是这样的:

  1. 当有人第一次调用它时,SaveAllAsync() 将在await xmlWriter.WriteStartDocumentAsync(); 行将控制权返回给调用者
  2. 然后...当他们等待SaveAllAsync()(或等待任务)...会发生什么? SaveAllAsync() 是否仍会停留在第一次等待直到被调用?因为没有涉及线程,我想是这样的......

【问题讨论】:

  • 您是否尝试过编写一个简单的程序来模拟这种行为并检查结果?
  • @Default 除非您已经知道它在做什么,否则实际上很难编写一个有效的模拟器来向您展示正在发生的事情。也就是说,有一百万本书/博客/文章/等。关于这个主题,这将使您对这里发生的事情有一个基本的了解。
  • @Servy 问题是关于一个有几个等待的方法。编写一个等待带有Task.Delays 的方法并输出一些调试消息的程序并不是很复杂.. 除非我遗漏了什么。
  • @Default 不,这并不复杂,但是能够通过这些打印的消息 is 确定幕后真正发生的事情。从明显观察到的行为中并不能立即看出真正发生了什么,至少除非您对如何开始有一个基本的了解。
  • 有人请检查我这个,但这个例子似乎不像是异步编码解决的问题?如果等待对异步方法的调用与简单的同步调用不同(就执行顺序而言)?即使异步线程正在完成工作,调用线程是否也不会阻塞?

标签: c# asynchronous async-await


【解决方案1】:

您可以将await 视为“暂停”async 方法,直到该操作完成。作为一种特殊情况,如果操作已经完成(或者非常快),那么await 将不会“暂停”该方法;它将立即继续执行。

所以在这种情况下(假设WriteStartDocumentAsync 尚未完成),await 将暂停该方法并将未完成的任务返回给调用者。请注意,async 方法返回的 Task 表示该方法;当方法完成时,则 Task 完成。

最终,WriteStartDocumentAsync 将完成,这将安排 async 方法的其余部分继续运行。在这种情况下,它将执行该方法的下一部分,直到下一个 await,当它再次暂停时,等等。最终,async 方法将完成,这将完成返回到的 Task表示该方法。

欲了解更多信息,我有一个async/await intro on my blog

【讨论】:

  • 因此,与此同时,当被调用方法的不同部分(一些等待和一些不等待)正在执行时,调用者(只要它已经等待被调用者)实际上可以返回控制给它的调用者。 ...这很好,因为上层代码可以保持控制(并检查是否有任何理由完全中止事情),但即使返回控制,等待的方法也保证得到一些时间来完成他们的任务
  • 另外,如果调用者调用了具有多个等待的方法,则完成该方法的每个语句的责任在于调用者。所以即使被调用者有多个等待,调用者最终要负责完成每一个语句,并且会要求.Net线程基础设施继续进入这个方法,试图完成一些事情,可能会一次又一次地等待,但是调用者在 calee 中等待的部分之后仍然负责执行代码。
  • @AndyzSmith "the awaited methods are guaranteed to get some time to do their tasks" 不,他们不是。某处的某些代码可能会拒绝放弃控制并执行阻塞等待,从而阻止这些延续运行。这就是为什么在使用异步代码时“一直异步”很重要的原因。如果某个地方的某人正在对其他东西进行阻塞等待,并且由于SynchronizationContext 正忙于阻塞等待而导致其他东西无法运行,则您遇到了“死锁”。
  • 任何类型的 UI 线程都不是阻塞例程吗?它无限期地运行,不再调用等待的方法,并且消耗无限循环,如果不理会永远。那么你的意思是,一些表现不佳的代码实际上可以抢占 SynchronizationContext 提供的抢占式多任务处理吗?
  • @AndyzSmith:@Servy 是正确的。此外,说“调用者最终负责完成每条语句”是不正确的。您可能想到的是您有一个 UI 线程上下文,并且 async 方法在 UI 线程上恢复的情况。 asyncawait 本身与线程无关,并且在控制台应用程序、ASP.NET 等中工作得很好(具有不同的线程语义)。
【解决方案2】:

斯蒂芬斯的回答当然是正确的。这是另一种可能会有所帮助的思考方式。

一大块代码的延续是代码完成后发生的事情。当您点击await 时,会发生两件事。首先,执行中的当前位置成为等待任务的延续。其次,控制离开当前方法并运行其他一些代码。其他代码可能是第一次调用的延续,或者可能是完全不同的东西,比如事件处理程序。

但是当对xmlWriter.WriteStartDocumentAsync() 的调用完成时;发生什么了?当前执行是否中断并返回到SaveAllAsync()

不清楚您所说的“完成”是什么意思。 WriteStartDocumentAsync 启动异步写入,可能在 I/O 完成线程上,并返回代表异步工作的 Task。正如我所说,等待该任务会做两件事。首先,这个任务的延续变成了代码的当前位置。其次,控制离开当前方法并运行其他一些代码。在这种情况下,任何名为 SaveAllAsync 的代码都会继续运行该调用。

现在让我们假设代码——SaveAllAsync 的调用者继续运行,并进一步假设您在一个带有 UI 线程的应用程序中,如 Windows 窗体应用程序或 WPF 应用程序。现在我们有两个线程:UI 线程和一个 IO 完成线程。 UI 线程正在运行 SaveAllAsync 的调用者,最终返回,现在 UI 线程只是坐在那里循环处理 Windows 消息以触发事件处理程序。

最终 IO 完成并且 IO 完成线程向 UI 线程发送一条说明“您现在可以运行此任务的延续”。如果 UI 线程忙,则该消息将排队;最终 UI 线程到达它并调用延续。控制在第一个await 之后恢复,然后您进入循环。

现在WriteStartElementAsync 被调用。它再次启动一些代码运行,这取决于 IO 完成线程上发生的事情(大概;它的工作方式取决于它,但这是一个合理的猜测),返回一个代表该工作的 Task 和 UI线程等待该任务。同样,执行中的当前位置被注册为该任务的延续,并且控制权返回给调用第一个延续的调用者——即 UI 线程的事件处理器。它继续愉快地处理消息,直到有一天 IO 线程向它发出信号并说嘿,您要求的工作在 IO 完成线程上完成,请调用此任务的继续,所以我们再次循环......

有意义吗?

【讨论】:

  • 但是当对xmlWriter.WriteStartDocumentAsync()的调用完成时;发生什么了?当前执行是否中断并返回到SaveAllAsync()
  • @Motig 不,它不会中断任何事情。如果有SynchronizationContext,那么将使用它。同步上下文特别是一种说法,“如果你给我一个委托,我可以在一组特定的条件下执行它。”通常这些“条件”意味着在特定线程中运行它,但并非总是如此。如果没有同步上下文,则该方法的其余部分将被安排在线程池中运行。如果存在 is 同步上下文,则通常意味着只要其他正在运行的东西完成了 it 正在做的事情,它就会运行。这就是阻塞等待会导致问题的原因。
  • @Motig:在一个内核上运行的两个线程,一个抢占另一个,async/await 的区别在于,在第一种情况下,抢占是不合作的。处理器决定谁在何时运行。对于任务,它更像是 Windows 3 风格的协作多任务; await 的意思是“我可以在这里停下来等待将来发生的事情;您继续进行其他工作,并让我知道什么时候发生了事情,以便我可以继续”。
  • @Motig:接下来会发生什么取决于等待任务的上下文。如果它是 UI 线程,那么正如我所说,希望触发延续的线程向 UI 线程发送消息。该消息位于队列中,当 UI 线程处理它时,继续运行。在没有 UI 线程的上下文中,延续可以在完成工作的线程上运行,或者它可以在新的工作线程上运行,或者其他什么。与任务关联的上下文决定了如何安全地运行延续。
  • @Motig:不用担心。 TPL 博客是文章的重要来源。此外,您可能想阅读我关于 CPS (blogs.msdn.com/b/ericlippert/archive/tags/…) 以及我们如何激励和设计异步的文章。 (blogs.msdn.com/b/ericlippert/archive/tags/async) 从底层做起;它们按从新到旧的顺序列出。
【解决方案3】:

每当一个函数是“异步”时,这意味着当在 System.Threading.Tasks.Task 上完成“等待”时,会发生两件主要事情:

  1. 执行中的当前位置成为等待任务的“延续”,这意味着在任务完成后,它将执行必要的操作以确保调用异步方法的其余部分。这项工作可以在特定线程、某个随机线程池线程或 UI 线程中完成,这取决于任务为“继续”而获得的 SynchronizationContext。

  2. 控制权返回到:

    • 如果它是 async 方法中的第一个 await,它将返回原始调用方法,并使用 System.Threading.Tasks.Task 表示异步方法(不是在异步方法中创建的任何任务)。它可以忽略它并继续执行,使用 Task.Result/Task.Wait() 等待它(注意不要阻塞 UI 线程),如果它本身是异步方法,甚至对其执行等待。

    • 如果它不是异步方法中的第一个等待,它只会返回到正在执行等待的最后一个任务的“继续”的线程中的任何处理程序。

所以回答:

当他们等待 SaveAllAsync()(或等待任务)时……会发生什么?

执行 await SaveAllAsync() 不一定会导致它卡在任何内部等待上。这是因为 SaveAllAsync() 上的等待只是退回到调用者调用 SaveAllAsync() 的任何方法,它可以像什么都没发生一样继续,就像 SaveAllAsync() 的内部等待退回到它一样。这使线程保持移动并能够(可能)在稍后的某个时间响应请求以运行 SaveAllAsync() 的第一个内部等待的“继续”:等待 xmlWriter.WriteStartDocumentAsync()'。这样,SaveAllAsync() 将一点一点地完成,不会卡住。

但是...如果您或其他一些更深入的代码曾经对等待返回的任何任务执行 Task.Result/Task.Wait(),如果“继续”尝试执行,这可能会导致事情卡住在与等待代码相同的线程上运行。

【讨论】:

  • This.issue 卡在等待同一个线程可以用 ConfigureAwait(false);这将允许任何线程继续。
【解决方案4】:

使用父方法示例的简单回答:

await SaveAllAsync();
string x = ""; // <- this will run only when SaveAllAsync() is done including all awaits

【讨论】:

  • Task.Delay(1); 是一个糟糕的代码行示例。它触发一个任务并立即忘记它。你最好用任何类型的同步代码替换这一行,比如Console.WriteLine 或其他。
  • 也许你打算等待那个 Delay() 但这仍然不能成为答案。
  • 第二行代码只是一个例子。我已将其编辑为更简单的内容。我的示例回答了第二个问题,例如SaveAllAsync() 不会在它的第一个内部等待中停止。
猜你喜欢
  • 1970-01-01
  • 2021-10-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多