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

When should Task.ContinueWith be called with TaskScheduler.Current as an argument?

我们正在使用 from Whosebug to produce a Task that completes as soon as the first of a collection of tasks completes successfully. Due to the non-linear nature of its execution, async/await is not really viable, and so this code uses ContinueWith() instead. It doesn't specify a TaskScheduler, though, which a number of sources 已经提到可能是危险的,因为它使用 TaskScheduler.Current 而大多数开发人员通常期望来自延续的 TaskScheduler.Default 行为。

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

最好将 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;
}

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

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

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

To avoid the default scheduler issue, you should always pass an explicit TaskScheduler to Task.ContinueWith and Task.Factory.StartNew.

来自 Stephen Cleary 的 post ContinueWith is Dangerous, Too

Stephen Toub 在他的 MSDN blog.

上有进一步的解释

我不得不吐槽一下,这给太多程序员带来了麻烦。每个旨在使线程处理看起来容易的编程辅助工具都会产生五个程序员没有机会调试的新问题。

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

async/await 模式是最新的,它真的 看起来很简单。但它的组合非常糟糕,异步海龟一直向下直到您到达 Main()。他们最终不得不在 C# 7.2 版中修复这个问题,因为每个人都被困在了这个问题上。但没有修复库中严重的 ConfigureAwait() 问题。它完全偏向于知道自己在做什么的库作者,值得注意的是,他们中的很多人都为 Microsoft 工作并修补 WinRT。

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

是的,您看到的建议很好。任务对于处理异步性很有用。当服务进入 "cloud" 并且延迟成为您不能再忽略的细节时,您必须处理的一个常见问题。如果您使用 ContinueWith() 那种代码,那么您总是关心执行延续的特定线程。由 TaskScheduler 提供,它不是由 FromCurrentSynchronizationContext() 提供的可能性很小。 async/await 就是这样发生的。

我当然不认为我有能力提供可靠的答案,但我会给我五分钱。

What is a specific example of a case where it would be best to pass TaskScheduler.Current into ContinueWith(), as opposed to TaskScheduler.Default?

假设您正在处理某个网络 api,该网络服务器自然会生成多线程。所以你需要妥协你的并行性,因为你不想使用你的网络服务器的所有资源,但同时你想加快你的处理时间,所以你决定制作具有较低并发级别的自定义任务调度程序,因为为什么没有。

现在你的 api 需要查询一些数据库并对结果进行排序,但是这些结果有数百万,所以你决定通过合并排序(分而治之)来完成,那么你需要所有的子任务此算法与您的自定义任务调度程序 (TaskScheduler.Current) 兼容,否则您最终将占用该算法的所有资源,并且您的网络服务器线程池将耗尽。

When to use TaskScheduler.Current, TaskScheduler.Default, TaskScheduler.FromCurrentSynchronizationContext(), or some other TaskScheduler

  • TaskScheduler.FromCurrentSynchronizationContext() - 特定于 WPF, Forms applications UI thread context,基本上当你使用它的时候 想在卸载一些工作后回到 UI 线程 非UI线程

示例取自 here

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() 传递给 TaskFactory 时,应该使用它或 Task.StartNew 方法,稍后您使用延续任务或内部任务(在我看来非常罕见)。

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

考虑以下示例:

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 的示例,因为延续委托实例可以在线程池上安全地执行。

您还可以使用自定义任务调度程序在您的库中实现自定义调度逻辑。例如,参见 Orleans 框架网站上的 Scheduler 页面。

更多信息请查看: