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



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

普遍的看法似乎是,您应该始终将显式 TaskScheduler 传递给 ContinueWith。但是,我还没有看到关于何时不同的任务计划程序最合适的明确解释。

最好将TaskScheduler.Current传递到ContinueWith()而不是TaskScheduler.Default的情况的具体例子是什么?做出此决定时是否有经验法则可循?

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

public static Task<T> FirstSuccessfulTask<T>(IEnumerable<Task<T>> tasks)
{
var taskList = tasks.ToList();
var tcs = new TaskCompletionSource<T>();
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<Task> 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<int> cpuHeavyCalculation = () => 0;
Action<Task> 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的示例,因为延续委托实例可以在线程池上安全地执行。

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

有关更多信息,请查看:

  • It's All About the SynchronizationContext 文章作者:Stephen Cleary
  • TaskScheduler, threads and deadlocks 文章作者:Cosmin Lazar
  • StartNew is Dangerous 文章作者:Stephen Cleary

我不得不咆哮一下,这让太多程序员陷入困境。 每一个旨在使线程看起来更容易的编程辅助工具都会产生五个程序员没有机会调试的新问题。

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

异步/等待模式是最新的,它使它看起来很容易。 但它的组成非常糟糕,异步海龟一直向下,直到你到达 Main()。 他们最终不得不在 C# 7.2 版本中修复这个问题,因为每个人都被困住了。 但是没有修复库中严重的 ConfigureAwait() 问题。 它完全偏向于图书馆作者知道他们在做什么,值得注意的是,他们中的许多人都在为Microsoft工作并修补 WinRT。

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

所以,是的,你看到的建议是一个很好的建议。 任务对于处理异步性很有用。 当服务移动到"云"和延迟时,您必须处理的一个常见问题是您再也不能忽视的细节。 如果你 ContinueWith() 那种代码,那么你总是关心执行延续的特定线程。 由TaskScheduler提供,它不是FromCurrentSynchronizationContext()提供的几率很低。 这就是异步/等待发生的方式。

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

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

使用TaskScheduler.Current的唯一原因:

为避免默认调度程序问题,应始终传递 明确TaskSchedulerTask.ContinueWithTask.Factory.StartNew

来自Stephen Cleary的帖子 继续下去也很危险。

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

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

最好

将 TaskScheduler.Current 传递到 ContinueWith() 而不是 TaskScheduler.Default 中的具体示例是什么?

想象一下,您正在开发一些 Web 服务器自然使多线程的 Web API。因此,您需要牺牲并行性,因为您不想使用Web服务器的所有资源,但同时又希望加快处理时间,因此您决定使用较低的并发级别制作自定义任务调度程序,因为为什么不呢。

现在你的api需要查询一些数据库并对结果进行排序,但这些结果是数百万个,所以你决定通过合并排序(分而治之)来完成,然后你需要这个算法的所有子任务都符合你的自定义任务调度程序(TaskScheduler.Current),否则你最终会占用算法的所有资源,你的Web服务器线程池将饿死。

何时使用 TaskScheduler.Current、TaskScheduler.Default

、TaskScheduler.FromCurrentSynchronizationContext() 或其他一些 TaskScheduler

  • TaskScheduler.FromCurrentSynchronizationContext() - 特定于 WPF, 表单应用程序 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)时应该使用它。

相关内容

最新更新