Task.ContinueWith() 运行在哪个调度程序上?

13

考虑以下代码:

// MyKickAssTaskScheduler is a TaskScheduler, IDisposable
using (var scheduler = new MyKickAssTaskScheduler())
{
    Task foo = new Task(() => DoSomething());
    foo.ContinueWith((_) => DoSomethingElse());
    foo.Start(scheduler);
    foo.Wait();
}

ContinueWith() 方法是否保证在我的调度器上运行?如果不是,它将使用哪个调度器?

3个回答

13

StartNew和ContinueWith将默认使用TaskScheduler.Current,而Current将返回默认的调度程序。当未从任务中调用时(MSDN)。

为了避免默认调度程序问题,您应始终向Task.ContinueWith和Task.Factory.StartNew传递显式的TaskScheduler。

ContinueWith也是危险的


谢谢。这是一个值得怀疑的设计决策还是我漏掉了什么?TPL 的任何人解释过吗? - bavaza
@bavaza - 对我来说也是新的,开始阅读以获取更深入的信息。 - Pranay Rana
1
@bavaza,TPL的Stephen Toub在这里提供了一些解释:http://blogs.msdn.com/b/pfxteam/archive/2012/09/22/new-taskcreationoptions-and-taskcontinuationoptions-in-net-4-5.aspx - noseratio - open to work
@Noseratio - 我已经阅读了它,但仍然对这种行为的有效性持怀疑态度 - 我有一个理由在非默认调度程序上运行第一个任务。为什么TPL决定继续执行(始终与我的任务顺序执行)应该在另一个调度程序上运行? - bavaza
@bavaza,我在这里回复了您的评论(https://dev59.com/TV0Z5IYBdhLWcg3wvSd2#31116891)。 - noseratio - open to work

4

ContinueWith()方法是否保证在我的调度程序上运行?如果不是,它将使用哪个调度程序?

不是的,它将使用传递给原始Task的调度程序。 ContinueWith将默认使用TaskScheduler.Current,在这种情况下是默认的线程池任务调度程序。您提供的上下文到task.Start之间没有传播到继续执行中使用的上下文。

来自源代码

public Task ContinueWith(Action<Task> continuationAction)
{
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
    return ContinueWith(continuationAction, 
                        TaskScheduler.Current, 
                        default(CancellationToken),
                        TaskContinuationOptions.None, ref stackMark);
}

我进行了一些测试,并发现我的续体运行在不同的调度程序上。我猜测这是因为当从任务内部调用时,TaskScheduler.Current返回默认的 .Net Framework 调度程序。请参见此处 - bavaza
我的自定义调度程序是 StaTaskScheduler 的一个变体,因此使用的是 STA 线程,而不是线程池中的线程。 - bavaza
@bavaza 你说得对,我进行了一些测试,调度程序没有流动。 - Yuval Itzchakov
@YuvalItzchakov,但它确实遵循了环境任务调度器的流程 :) - noseratio - open to work
@Noseratio 环境中的 TaskScheduler.Current 是什么意思?它是否会从 ThreadPoolTaskScheduler 更改? - Yuval Itzchakov
1
@YuvalItzchakov,它是运行当前执行任务的任务调度程序。因此,对于通过TPL API在该任务内创建的任何任务,它都变得环境友好,您不需要显式指定调度程序(除了Task.Run,它始终使用TaskScheduler.Default)。例如,请参见我的答案中的代码。 - noseratio - open to work

4

@Noseratio - 已经阅读,但仍怀疑这种行为的有效性 - 我在非默认调度程序上运行第一个任务是有原因的。为什么TPL决定始终顺序运行于我的任务之后的继续应该在另一个调度程序上运行?

我同意 - 这不是最好的设计 - 但我想默认使用 TaskScheduler.Current 是为了保持 ContinueWithTask.Factory.StartNew 的一致性,后者首先默认使用 TaskScheduler.Current。Stephen Toub 解释了后者的设计决策:

在许多情况下,这是正确的行为。例如,假设您正在实现递归分治问题,其中您有一个任务应该处理一些工作块,并将其工作细分并安排任务来处理这些块。如果该任务正在运行表示特定线程池的调度程序上,或者如果它正在运行具有并发限制的调度程序上等等,那么您通常希望随后创建的任务也在同一个调度程序上运行。
因此,ContinueWith 使用当前(环境)TaskScheduler.Current,而不是前置任务的 TaskScheduler。如果这对您造成了问题,并且您无法显式指定任务调度程序,则可以使用以下方法解决。您可以使自定义任务调度程序成为特定范围的环境调度程序,如下所示:
using (var scheduler = new MyKickAssTaskScheduler())
{
    Task<Task> outer = new Task(() => 
    {
       Task foo = new Task(() => DoSomething());
       foo.ContinueWith((_) => DoSomethingElse());
       foo.Start(); // don't have to specify scheduler here
       return foo;
    }

    outer.RunSynchronously(scheduler);
    outer.Unwrap().Wait();
}

请注意,outerTask<Task>,因此需要使用 outer.Unwrap()。您也可以使用 outer.Result.Wait(),但这两种方法在语义上存在一些差异,特别是如果您使用了 outer.Start(scheduler) 而不是 outer.RunSynchronously(scheduler)

很酷的技巧 :) 。不过,我认为我还是会坚持明确指定调度程序或完全避免使用 ContinueWith()... - bavaza
1
@bavaza,如果你可以完全避免使用ContinueWith,而改用await,那将是最好的决定。你可以围绕一个带有自定义SynchronizationContext的线程来建模你的调度程序(这将使它支持await),然后只需执行TaskScheduler.FromCurrentSynchronizationContext即可。 - noseratio - open to work

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