等待异步模式和工作窃取线程

4
我正在尝试理解async/await模式的底层机制,读完以下杰尼弗·马斯曼(Jennifer Marsman)的博客文章《.NET 4.0中的工作窃取》后,我认为我已经理解了其中的核心。我的理解如下:
1 - 线程池有一个全局队列以及每个线程都有自己的本地队列。
2 - 请求进入全局队列,然后线程池中的线程1(T1)获取到请求。
3 - 这个请求是一个异步\等待方法。一旦遇到await关键字,就会创建一个包含书签(回调函数)的任务(假设该任务尚未完成),并将此任务放置在T1的本地队列中。T1返回到线程池。
4 - 当任务完成时,如果T1没有忙碌,T1将处理该请求。但是,如果T1忙碌,另一个线程(称之为T2)实际上可能从T1的本地队列中窃取此任务。
这就是我的问题所在。这怎么被禁止了呢?我读到的所有内容都表明,async\await不会改变线程上下文。请参见链接MSDN explanation of async\await。这也是有道理的,因为在MVC应用程序中,请求绑定到一个线程上。这意味着如果请求发送到异步操作方法,则我希望初始任务和后续任务都由同一个线程池线程完成。工作窃取线程怎么不会干扰这个呢?感谢任何见解。

从广泛使用的 Task#ConfigureAwait(false) 来看,我认为异步/等待确实会改变其同步上下文。据我所知,ASP.NET 确实是一个特殊情况,但我不知道它是如何处理的。 - Jeroen Vannevel
2个回答

7
这里有三个半独立的系统在工作:线程池(带有工作窃取队列)、ASP.NET请求上下文和async/await。线程池的工作方式如您所描述:每个线程都有自己的队列,但如果需要,可以从其他线程的队列中窃取。但是,这实际上与async/await在ASP.NET上的工作方式几乎没有关系。在很大程度上,您可以完全忽略工作窃取队列的工作原理,因为逻辑抽象是单个线程池和单个队列。工作窃取队列只是一种优化。
ASP.NET请求上下文管理诸如HttpContext.Current、安全性和文化等事物。它不绑定到特定线程,但同一时间只允许一个线程位于上下文中。这种模式对于旧式异步请求以及新式async请求都是正确的。请注意,请求仅在同步请求期间绑定到线程,而对于异步请求(从未如此),请求不会在开始到结束时绑定到线程。ASP.NET请求上下文被实现为同步上下文——具体来说,是AspNetSynchronizationContext的实例。
当你的代码使用await等待一个未完成的Task时,默认情况下,await会捕获当前上下文(即SynchronizationContext.Current,除非它为null,此时为当前的TaskScheduler)。当Task完成后,async方法将在该上下文中继续执行。我在我的博客中更详细地描述了这种行为。你可以把async/await看作是“线程不可知”的;也就是说,它们不一定会在不同的线程上恢复,也不一定会在相同的线程上恢复。它们把所有线程决策留给了被捕获的上下文。

还有一点需要注意的是,有两种不同类型的Task,Promise Tasks和Delegate Tasks(我在我的博客中有描述)。只有Delegate Tasks实际上有要运行的代码,并且都会排队到线程池中。因此,当await决定暂停其方法时,它没有要运行的代码,并且此时没有任何东西排队;相反,它设置了一个回调(继续执行),将在将来排队方法的其余部分。

当等待的任务完成时,该回调 / 继续执行将运行,将剩余的async方法排队到捕获的上下文中。理论上,这可能会将其排队到线程池中,但实际上几乎总是采用快捷方式:完成任务的线程通常本身就是线程池线程,因此它只需直接进入请求上下文,然后恢复执行async方法,而无需实际排队到任何地方。

在绝大多数情况下,工作窃取队列并不起作用。只有在线程池超载的情况下才会发生这种情况。
但请注意,一个异步处理程序可能会从一个线程开始并在另一个线程上继续运行,这是完全可能的(也很常见)。通常情况下这不是问题,因为请求上下文得以保留,但像线程局部变量这样的线程相关结构将不能正确地工作。

1
谢谢,您的回答和博客帮了我很多。 - freud

2
处理方式是每个线程都有一个全局的SyncronisationContext对象与其关联。该对象负责处理如何恢复任务,因为不同情况需要不同的要求。例如,如果你正在进行GUI工作,为了使挂起透明,任务必须在同一线程上恢复。然而,如果你在没有设置特殊上下文的线程中,则使用默认的SyncronisationContext,它会在线程池上恢复。因此,整个线程池共享一个SyncronisationContext,其中任何没有设置的线程都可以使用。但GUI线程和MVC操作线程会为每个线程设置特殊的SyncronisationContext,以确保它在同一线程上恢复。还可以使用ConfigureAwait设置任务以在不同的SyncronisationContext下恢复。
简而言之:工作窃取无法跨越SyncronisationContext,但一个SyncronisationContext可以包含多个线程以允许工作窃取。

谢谢,你的回答让我更多地了解了SynchronizationContext,现在事情变得更清晰了。 - freud

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接