SynchronizationContext和TaskScheduler之间的概念差异是什么?

49

Stephen Toub的博客指出:

同步上下文(SynchronizationContext)和任务调度器(TaskScheduler)都是表示“调度程序”的抽象层级,你可以将一些工作交给它,然后它会决定何时何地运行该工作。有许多不同形式的调度程序。例如,线程池(ThreadPool)就是一个调度程序:你调用ThreadPool.QueueUserWorkItem来提供要运行的委托,该委托被排队,而线程池的一个线程最终选择并运行该委托。你的用户界面也有一个调度程序:消息泵(Message Pump)。

因此,System.Reactive.Concurrency.EventLoopSchedulerDispatcherThreadPoolTaskSchedulerSyncrhonizationContextReactive Extensions的IScheduler实现都是在这个意义上的“调度程序”。

它们之间有什么区别?

它们为什么都是必要的?我想我懂得EventLoop、Dispatcher、ThreadPool和IScheduler,但TaskScheduler和SyncrhonizationContext对我来说仍然不清楚。

Stephen Cleary的精彩文章解释了同步上下文,我认为我已经理解了。那么为什么我们需要TaskScheduler不清楚。

请解释或指出相关资料。


可能有许多原因,如答案所示。还有一种原因尚未提及,我在这篇MSDN博客文章中找到了它: http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259082.aspx 文章说,SyncrhonizationContext.Post异步方法在执行工作项时不提供通知。该文章建议如何添加一个使用TaskCompletionSource返回Task的扩展方法。 - Michael Kariv
3个回答

29

我刚刚在阅读 Jeffrey Ritcher 的 CLR via C# 一书,感谢他让我能够简单地解释相关主题(假设我并不完全同意答案中的所有细节)。

首先,TaskScheduler 对象负责执行计划任务。FCL 提供了两种派生自 TaskScheduler 的类型:线程池任务调度程序同步上下文任务调度程序。默认情况下,所有应用程序都使用线程池任务调度程序。该任务调度程序将任务安排到线程池的工作线程中。可以通过查询 TaskScheduler 的静态 Default 属性获取对默认任务调度程序的引用。

同步上下文任务调度程序通常用于支持图形用户界面的应用程序。该任务调度程序将所有任务安排到应用程序的 GUI 线程上,以便所有任务代码都可以成功更新诸如按钮、菜单项等 UI 组件。 同步上下文任务调度程序根本不使用线程池。可以通过查询 TaskScheduler 的静态 FromCurrentSynchronizationContext 方法获取同步上下文任务调度程序的引用。

如你从SynchronizationContextTaskScheduler的实现中可以看到,内部使用了SynchronizationContext字段。 FCL定义了一个基类,称为System.Threading.SynchronizationContext,它解决了所有这些问题:
  • GUI应用程序强制执行线程模型,其中创建UI元素的线程是唯一允许更新该UI元素的线程。这是一个问题,因为如果您的代码尝试通过线程池线程更新UI元素,则会引发异常。不知何故,线程池线程必须使GUI线程更新UI元素。
  • ASP.NET应用程序允许任何线程执行任何操作。当线程池线程开始处理客户端的请求时,它可以假定客户端的文化并返回特定于文化的数字、日期和时间格式。此外,Web服务器可以假定客户端的身份,以便服务器只能访问客户端被允许访问的资源。当线程池线程生成异步操作时,它可能会由另一个线程池线程完成,后者将处理异步操作的结果。在代表原始客户端请求执行此工作时,文化和身份需要“流动”到新的线程池线程,以便代表客户端执行的任何附加工作都使用客户端的文化和身份信息。

简单地说,一个基于SynchronizationContext的对象连接了应用程序模型与其线程模型。FCL定义了几个派生自SynchronizationContext的类,但通常您不会直接处理这些类; 实际上,其中许多类没有公开暴露或记录。

对于大多数应用程序开发者而言,他们无需了解SynchronizationContext类的任何内容。当您await一个Task时,会获取调用线程的SynchronizationContext对象。当线程池线程完成任务时,将使用SynchronizationContext对象,确保应用程序模型的正确线程模型。因此,当GUI线程await一个Task时,紧随await运算符的代码也一定会在GUI线程上执行,从而允许该代码更新UI元素。对于ASP.NET应用程序,跟随await运算符的代码保证在具有客户端文化和主体信息关联的线程池线程上执行

如果您有特殊的任务调度需求,当然可以定义一个从TaskScheduler派生的自定义类。Microsoft提供了许多示例代码和任务调度器源代码,包括在Parallel Extensions Extras包中。例如:IOTaskScheduler, LimitedConcurrencyLevelTaskScheduler, OrderedTaskScheduler, PrioritizingTaskScheduler, ThreadPerTaskScheduler


17

每个平台都有自己的“调度程序”,并且它们都有围绕它们的抽象。例如,WinForms使用消息泵,WPF使用另一个在“Dispatcher”中抽象的消息泵,线程池是另一个在“ThreadPool”中抽象的“调度程序”,这些(和其他一些)都是较低级别的调度程序。

一个任务和任务调度程序希望任务的用户不必考虑在这些较低级别上调度任务(当然,您可以以抽象的方式)。您应该能够启动一个任务,而所谓的“环境”调度程序应该会照顾好它。例如,TaskFactory.StartNew(()=>{LengthyOperation()}) 应该在任何平台下工作。这就是 SynchronizationContext 的作用。它知道当前运行框架中涉及哪些较低级别的调度程序。将其传递给 TaskScheduler,该调度程序可以通过与当前运行的框架(请参阅 SynchronizationContext)关联的较低级别调度程序安排任务(可能是在 ThreadPool 上),并安排后续操作以满足同步要求。例如,尽管您希望任务在 ThreadPool 中运行,但您可能希望后续操作在 UI 线程中运行。

重要的是要知道, TaskScheduler 是多个其他调度程序的抽象。这不是它存在的唯一原因,但这是这种“额外”抽象的原因之一。


1
同步上下文(SynchronizationContext)的出现是为了知道当前正在运行的框架中涉及到哪些较低级别的调度器 - 如果它知道,那么我们为什么还需要TaskScheduler?例如:所有排队到WindowsFormsSynchronizationContext的委托都逐个执行;它们由特定的UI线程按照排队顺序执行。如果同步上下文可以安排任务进行执行,那么我仍然看不出需要TaskScheduler的原因。 - user4205580
传递给TaskScheduler的是什么? - user4205580
这里有一个简单的解释,我认为它能够澄清一些话题 - http://blogs.msdn.com/b/pfxteam/archive/2009/09/22/9898090.aspx 看起来TaskScheduler本身并没有什么特别之处,它只是让代码更加清晰 - 我们可以使用TaskScheduler将任务传递给它,而不是将委托传递给SynchronizationContext,后者知道当前平台的详细信息。不确定它是否符合您的解释。 - user4205580
1
你能给我指一个例子,用于“虽然你想让你的任务在线程池中运行,但你可能希望继续在UI线程中运行。”吗? - variable
2
@variable 如果您需要处理UI(例如更改控件值或更改可见性),则需要在UI线程上执行。 - Peter Ritchie
显示剩余2条评论

13

虽然如引述所说,SynchronizationContext和TaskScheduler都是代表“调度程序”的抽象,但在我看来,SynchronizationContext的抽象程度(因此其API)与TaskScheduler不同。在某种意义上,SynchronizationContext是更通用的API,因为Post/Send采用简单的方法委托。

另一方面,TaskScheduler是一个特定于TPL的抽象,因此提供了处理Task对象的方法,例如QueueTask。使用同步上下文代替任务调度程序(即具有TPL特定实现的SynchronizationContext)会使任务调度变得更加繁琐(当然,在TPL上下文中,它将是弱类型的API)。因此,TPL设计者选择建模一个对TPL有意义的抽象调度程序API(这正是抽象的目的 - 对吧?)- 当然,为了弥合差距,FCL包含一个名为SynchronizationContextTaskScheduler的内部类,它是SynchronizationContext的TaskScheduler实现的包装器。

SynchronizationContext在.NET 2.0中引入,而TPL在.NET 4中引入。有趣的是,如果序列反过来,即.NET 2.0时存在TPL,FCL设计者会选择什么。在我看来,可以通过将委托建模为特定的任务专业化,而使用TaskScheduler代替SynchrinizationContext。


1
谢谢Vinay。我也认为其中一个只是比另一个新一些。仍然让我困惑的是,每个异步库似乎都坚持设计自己的抽象,即使它们属于同一代。看看Rx(反应式扩展),它也是NET 4。他们有IScheduler和许多实现。与任务无关。这次它不是抽象类而是接口。鉴于这么多库,每个库都有自己的优势,但是重叠程度非常大,我希望会有一本书或一篇文章来梳理这一切。 - Michael Kariv
1
据我所知,Rx不是FCL的一部分 - 它是在.NET 4之后开发和发布的库。它可能会被添加到下一个版本的Fx中,也可能不会。话虽如此,我相信你会继续看到同一概念的多个抽象,仅仅因为一个抽象可能不能很好地适应所有情况(适应性不是从功能角度而是从接口角度考虑)。 - VinayC
4
SynchronizationContext 更多是关于执行亲和性的要求。例如,如果您想更新控件,则需要在 UI 线程上执行。这个要求被抽象到了 SychronizationContext 中。如何安排各个任务之间的调度以及它们在哪里运行(可能需要同步上下文)被抽象到了 TaskScheduler 中。这些是相关但正交的抽象。 - Peter Ritchie
6
@PeterRitchie说,SynchroContext关注的是“在哪里”,而Scheduler关注的是“在哪里”和“什么顺序”。因此,它们不是正交的。如果Scheduler只关注“什么顺序”,那么它们就会真正正交。但事实上,它们存在重叠。我个人认为,SynCtx是实现(任务)调度器的“低级”、“特定案例”的构建块。但不是正交的! - quetzalcoatl

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