为什么TaskScheduler.Current是默认的TaskScheduler?

任务并行库很棒,在过去的几个月里我用了很多。 但是,有一件事真的让我感到困扰: TaskScheduler.Current是默认的任务调度器,而不是TaskScheduler.Default 。 乍一看,这在文档和样本中是绝对不明显的。

Current可能导致微妙的错误,因为它的行为正在改变,取决于你是否在另一个任务。 哪个不容易确定。

假设我正在写一个asynchronous方法库,使用基于事件的标准asynchronous模式来标记原始同步上下文的完成情况,与XxxAsync方法在.NET Framework(例如DownloadFileAsync )中完全相同。 我决定使用任务并行库进行实现,因为使用以下代码很容易实现此行为:

 public class MyLibrary { public event EventHandler SomeOperationCompleted; private void OnSomeOperationCompleted() { var handler = SomeOperationCompleted; if (handler != null) handler(this, EventArgs.Empty); } public void DoSomeOperationAsync() { Task.Factory .StartNew ( () => Thread.Sleep(1000) // simulate a long operation , CancellationToken.None , TaskCreationOptions.None , TaskScheduler.Default ) .ContinueWith (t => OnSomeOperationCompleted() , TaskScheduler.FromCurrentSynchronizationContext() ); } } 

到目前为止,一切运作良好。 现在,让我们通过点击WPF或WinForms应用程序中的一个button来调用这个库:

 private void Button_OnClick(object sender, EventArgs args) { var myLibrary = new MyLibrary(); myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse(); myLibrary.DoSomeOperationAsync(); } private void DoSomethingElse() { ... Task.Factory.StartNew(() => Thread.Sleep(5000)/*simulate a long operation*/); ... } 

在这里,编写库调用的人在操作完成时select启动新的Task 。 没什么特别的 他或她跟随在网上find的例子,并简单地使用Task.Factory.StartNew而不指定TaskScheduler (并且在第二个参数中没有简单的重载指定它)。 DoSomethingElse方法在单独调用时可以正常工作,但一旦被事件调用,UI就会冻结,因为TaskFactory.Current将重用库继续中的同步上下文任务调度程序。

找出这可能需要一些时间,特别是如果第二个任务调用埋在一些复杂的调用堆栈。 当然,一旦你知道每件事情是如何工作的,那么这里的解决方法很简单:总是为TaskScheduler.Default指定你希望在线程池上运行的任何操作。 然而,也许第二个任务是由另一个外部库启动的,不知道这个行为,并且天真地使用StartNew而没有特定的调度器。 我期待这种情况是相当普遍的。

在我的脑袋周围,我无法理解编写TPL的团队select使用TaskScheduler.Current而不是TaskScheduler.Default作为默认值:

  • 一点也不明显, Default不是默认! 而且文件严重缺乏。
  • Current使用的真正的任务调度器取决于调用堆栈! 这种行为很难保持不变。
  • 使用StartNew指定任务调度程序很麻烦,因为您必须首先指定任务创build选项和取消标记,从而导致长可读性较差的行。 这可以通过编写扩展方法或创build使用DefaultTaskFactory来缓解。
  • 捕获调用堆栈具有额外的性能成本。
  • 当我真的想要一个任务依赖另一个父运行任务时,我更愿意明确地指定它来简化代码读取,而不是依赖调用堆栈魔术。

我知道这个问题可能听起来很主观,但我不能find一个好的客观的论点,为什么这个行为是这样的。 我确定我在这里错过了一些东西:这就是我转向你的原因。

我认为目前的行为是有道理的。 如果我创build自己的任务调度程序,并启动一些启动其他任务的任务,我可能希望所有任务都使用我创build的调度程序。

我同意奇怪的是,有时从UI线程开始一个任务使用默认调度程序,有时不会。 但是我不知道如果我devise的话怎么做会更好。

关于你的具体问题:

  • 我认为在指定的调度程序上启动新任务的最简单的方法是new Task(lambda).Start(scheduler) 。 这有一个缺点,就是如果任务返回一些东西,你必须指定types参数。 TaskFactory.Create可以推断出你的types。
  • 您可以使用Dispatcher.Invoke()而不是使用TaskScheduler.FromCurrentSynchronizationContext()

[编辑]下面只解决了Task.Factory.StartNew使用的调度器的Task.Factory.StartNew
但是, Task.ContinueWith有一个硬编码的TaskScheduler.Current 。 [/编辑]

首先,有一个简单的解决scheme可用 – 请参阅这篇文章的底部。

这个问题背后的原因很简单:不仅有一个默认的任务调度器( TaskScheduler.Default ),而且还有一个TaskFactoryTaskFactory.Scheduler )的默认任务调度器。 这个默认调度程序可以在创build时在TaskFactory的构造函数中指定。

但是, TaskFactory背后的Task.Factory创build如下:

 s_factory = new TaskFactory(); 

正如你所看到的,没有指定TaskFactorynull用于默认的构造函数 – 更好的是TaskScheduler.Default (文档声明使用“Current”,它具有相同的结果)。
这又导致了TaskFactory.DefaultScheduler (私有成员)的实现:

 private TaskScheduler DefaultScheduler { get { if (m_defaultScheduler == null) return TaskScheduler.Current; else return m_defaultScheduler; } } 

在这里您应该看到能够识别出这种行为的原因:由于Task.Factory没有默认的任务调度器,所以将使用当前的调度器。

那么为什么我们不碰到NullReferenceExceptions那么当没有Task正在执行(即我们没有当前的TaskScheduler)呢?
原因很简单:

 public static TaskScheduler Current { get { Task internalCurrent = Task.InternalCurrent; if (internalCurrent != null) { return internalCurrent.ExecutingTaskScheduler; } return Default; } } 

TaskScheduler.Current默认为TaskScheduler.Default

我会称这是一个非常不幸的实现。

但是,有一个简单的解决scheme可用:我们可以简单地将Task.Factory的默认TaskScheduler设置为TaskScheduler.Default

 TaskFactory factory = Task.Factory; factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default }); 

我希望我可以帮助我的回应,虽然这是相当晚的:-)

而不是Task.Factory.StartNew()

考虑使用: Task.Run()

这将始终在线程池线程上执行。 我只是遇到了同样的问题,我认为这是处理这个问题的好方法。

看到这个博客条目: http : //blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

一点也不明显,默认不是默认! 而且文件严重缺乏。

Default是默认的,但并不总是“ Current

正如其他人已经回答的那样,如果您希望任务在线程池上运行,则需要通过将Default调度程序传递到TaskFactoryStartNew方法来显式设置Current调度程序。

既然你的问题涉及到一个图书馆,我认为答案是,你不应该做任何将改变你的库以外的代码看到的Current调度程序。 这意味着您在引发SomeOperationCompleted事件时不应该使用TaskScheduler.FromCurrentSynchronizationContext() 。 相反,做这样的事情:

 public void DoSomeOperationAsync() { var context = SynchronizationContext.Current; Task.Factory .StartNew(() => Thread.Sleep(1000) /* simulate a long operation */) .ContinueWith(t => { context.Post(_ => OnSomeOperationCompleted(), null); }); } 

我甚至不认为你需要在Default调度程序上明确地启动你的任务 – 让调用者确定Current调度程序,如果他们想。

我花了几个小时试图debugging一个奇怪的问题,我的任务在UI线程上被安排,即使我没有指定它。 原来这个问题正是你的示例代码演示的:在UI线程上计划了一个任务延续,在这个延续的某个地方,开始了一个新的任务,然后在UI线程上进行调度,因为当前执行的任务有一个具体的TaskScheduler设置。

幸运的是,这是我自己的所有代码,所以我可以通过确保我的代码在启动新任务时指定TaskScheduler.Default来修复它,但是如果你不那么幸运,我的build议是使用Dispatcher.BeginInvoke而不是使用UI调度。

所以,而不是:

 var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); var task = Task.Factory.StartNew(() => Thread.Sleep(5000)); task.ContinueWith((t) => UpdateUI(), uiScheduler); 

尝试:

 var uiDispatcher = Dispatcher.CurrentDispatcher; var task = Task.Factory.StartNew(() => Thread.Sleep(5000)); task.ContinueWith((t) => uiDispatcher.BeginInvoke(new Action(() => UpdateUI()))); 

尽pipe如此,它的可读性稍差。