什么时候应该使用TaskScheduler.Current作为参数调用Task.ContinueWith?

我们正在使用StackOverflow中的此代码片段来生成一个任务,该任务在第一个任务集合成功完成后立即完成。 由于其执行的非线性特性, async/await不是真的可行,因此这段代码使用ContinueWith()代替。 但是,它没有指定TaskScheduler, 许多 来源提到它可能很危险,因为当大多数开发人员通常期望来自continuation的TaskScheduler.Default行为时,它使用TaskScheduler.Current

流行的智慧似乎是你应该总是将一个显式的TaskScheduler传递给ContinueWith。 但是,我还没有看到关于何时最合适的TaskScheduler的明确解释。

什么是最好将TaskScheduler.Current传递给ContinueWith()的情况的具体示例,而不是TaskScheduler.Default ? 做出这个决定时是否有遵循的经验法则?

对于上下文,这是我所指的代码片段:

 public static Task FirstSuccessfulTask(IEnumerable<Task> tasks) { var taskList = tasks.ToList(); var tcs = new TaskCompletionSource(); int remainingTasks = taskList.Count; foreach(var task in taskList) { task.ContinueWith(t => if(task.Status == TaskStatus.RanToCompletion) tcs.TrySetResult(t.Result)); else if(Interlocked.Decrement(ref remainingTasks) == 0) tcs.SetException(new AggregateException( tasks.SelectMany(t => t.Exception.InnerExceptions)); } return tcs.Task; } 

您可能需要选择适合执行委托实例执行的操作的任务计划程序。

考虑以下示例:

 Task ContinueWithUnknownAction(Task task, Action actionOfTheUnknownNature) { // We know nothing about what the action do, so we decide to respect environment // in which current function is called return task.ContinueWith(actionOfTheUnknownNature, TaskScheduler.Current); } int count; Task ContinueWithKnownAction(Task task) { // We fully control a continuation action and we know that it can be safely // executed by thread pool thread. return task.ContinueWith(t => Interlocked.Increment(ref count), TaskScheduler.Default); } Func cpuHeavyCalculation = () => 0; Action printCalculationResultToUI = task => { }; void OnUserAction() { // Assert that SynchronizationContext.Current is not null. // We know that continuation will modify an UI, and it can be safely executed // only on an UI thread. Task.Run(cpuHeavyCalculation) .ContinueWith(printCalculationResultToUI, TaskScheduler.FromCurrentSynchronizationContext()); } 

您的FirstSuccessfulTask()可能是您可以使用TaskScheduler.Default的示例,因为可以在线程池上安全地执行continuation委托实例。

您还可以使用自定义任务计划程序在库中实现自定义计划逻辑。 例如,请参阅Orleans框架网站上的“ 计划程序”页面。

有关更多信息,请查看

  • 这是关于 Stephen Cleary 的SynchronizationContext文章的全部内容
  • Cosmin Lazar的TaskScheduler,线程和死锁文章
  • StartNew是 Stephen Cleary的危险文章

我不得不咆哮,这让太多的程序员陷入困境。 旨在使线程变得容易的每一个编程辅助工具都会产生五个程序员无法调试的新问题。

BackgroundWorker是第一个,一个谦虚而明智的尝试来隐藏并发症。 但没有人意识到工作者在线程池上运行所以不应该用I / O占用自己。 每个人都错了,没有多少人注意到。 并且忘记检查RunWorkerCompleted事件中的e.Error,在线程代码中隐藏exception是包装器的普遍问题。

async / await模式是最新的,它让它看起来很简单。 但它组成非常糟糕,异步乌龟一直向下,直到你到达Main()。 他们最终必须在C#版本7.2中修复它,因为每个人都被卡住了。 但是没有修复库中严重的ConfigureAwait()问题。 它完全偏向于图书馆作者知道他们在做什么,值得注意的是,他们中的很多人都在为微软工作并修补WinRT。

Task类弥合了两者之间的差距,其设计目标是使其非常可组合。 好的计划,他们无法预测程序员将如何使用它。 但也是一个负担,激励程序员继续使用ContinueWith()风暴来将任务粘合在一起。 即使这样做没有意义,因为这些任务只是按顺序运行。 值得注意的是,他们甚至添加了一个优化,以确保延续在同一个线程上运行,以避免上下文切换开销。 很好的计划,但创建了这个网站命名的不可攻击的问题。

所以是的,你看到的建议很好。 任务对于处理异步性非常有用。 当服务进入“云”并且延迟成为您不能再忽视的细节时,您必须处理的常见问题。 如果你继续使用那种类型的代码,那么你总是关心执行延续的特定线程。 由TaskScheduler提供,它不是FromCurrentSynchronizationContext()提供的低概率。 这是异步/等待发生的原因。

如果当前任务是子任务,那么使用TaskScheduler.Current将意味着调度程序将是它所在的任务所安排的; 如果不在另一个任务中, TaskScheduler.Current将是TaskScheduler.Default ,因此使用ThreadPool。

如果您使用TaskScheduler.Default ,那么它将始终转到ThreadPool。

您使用TaskScheduler.Current的唯一原因:

要避免出现默认调度程序问题,应始终将显式TaskScheduler传递给Task.ContinueWithTask.Factory.StartNew

来自Stephen Cleary的postContinueWith是Dangerous,Too 。

Stephen Toub在他的MSDN博客上有进一步的解释。

我当然不认为我有能力提供防弹答案,但我会给我五美分。

什么是最好将TaskScheduler.Current传递给ContinueWith()的情况的具体示例,而不是TaskScheduler.Default?

想象一下,你正在开发一些web服务器自然会制作multithreading的web api。 所以你需要妥协你的并行性,因为你不想使用你的web服务器的所有资源,但同时你想加快你的处理时间,所以你决定使用降低的并发级别的自定义任务调度程序,因为为什么不。

现在你的api需要查询一些数据库并订购结果,但这些结果是数百万,所以你决定通过Merge Sort(分而治之)来做,然后你需要这个算法的所有子任务都适合你的自定义任务调度程序( TaskScheduler.Current )因为否则您将最终获取算法的所有资源,并且您的Web服务器线程池将会饿死。

何时使用TaskScheduler.Current,TaskScheduler.Default,TaskScheduler.FromCurrentSynchronizationContext()或其他一些TaskScheduler

  • TaskScheduler.FromCurrentSynchronizationContext() – 特定于WPF,Forms应用程序的UI线程上下文,你基本上在你想要在卸载一些工作到非UI线程后回到UI线程时使用它

从这里取的例子

 private void button_Click(…) { … // #1 on the UI thread Task.Factory.StartNew(() => { … // #2 long-running work, so offloaded to non-UI thread }).ContinueWith(t => { … // #3 back on the UI thread }, TaskScheduler.FromCurrentSynchronizationContext()); } 
  • TaskScheduler.Default – 几乎所有的时候你没有任何特定的要求,边缘情况要整理。
  • TaskScheduler.Current – 我想我已经给出了一个上面的通用示例,但一般来说,当你有自定义调度程序或者你明确地将TaskScheduler.FromCurrentSynchronizationContext()传递给TaskFactoryTask.StartNew方法时,应该使用它,之后你使用延续任务或内心的任务(非常罕见的imo)。