Task.Factory.StartNew()是否保证使用另一个线程而不是调用线程?

Erw*_*yer 74 c# multithreading locking task task-parallel-library

我从一个函数开始一个新的任务,但我不希望它在同一个线程上运行.我不关心它运行在哪个线程上,只要它是一个不同的线程(因此这个问题中给出的信息没有帮助).

我保证TestLock在允许Task t再次输入之前,以下代码将始终退出吗?如果没有,建议的设计模式是什么来防止再次出现?

object TestLock = new object();

public void Test(bool stop = false) {
    Task t;
    lock (this.TestLock) {
        if (stop) return;
        t = Task.Factory.StartNew(() => { this.Test(stop: true); });
    }
    t.Wait();
}
Run Code Online (Sandbox Code Playgroud)

编辑:基于Jon Skeet和Stephen Toub的以下答案,确定性地防止重入的一种简单方法是传递CancellationToken,如此扩展方法所示:

public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) 
 {
    return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 83

我邮寄了Stephen Toub-- PFX团队的成员- 关于这个问题.他很快就回到了我身边,带着很多细节 - 所以我只是复制并粘贴他的文字.我没有引用它,因为阅读大量的引用文本最终变得不如香草黑白色,但真的,这是斯蒂芬 - 我不知道这么多东西:)我做了这个回答社区维基反映下面所有的好处并不是我的内容:

如果您调用已完成Wait()任务,则不会有任何阻塞(如果任务使用TaskStatus以外的任务完成RanToCompletion,或者以nop形式返回,则只会抛出异常).如果你调用Wait()一个已经在执行的任务,它必须阻塞,因为它没有其他任何可以合理做的事情(当我说阻止时,我包括真正的基于内核的等待和旋转,因为它通常会混合使用两者).同样,如果您调用Wait()具有Created或者WaitingForActivation状态的任务,它将阻塞,直到任务完成.这些都不是正在讨论的有趣案例.

有趣的情况是当你调用Wait()状态中的Task时WaitingToRun,意味着它之前已经排队到TaskScheduler,但是TaskScheduler还没有实际运行Task的委托.在这种情况下,调用Wait将询问调度程序是否可以通过调用调度程序的TryExecuteTaskInline方法在当前线程上运行任务.这称为内联.调度程序可以选择通过调用来内联任务base.TryExecuteTask,或者它可以返回'false'以指示它没有执行任务(通常这是通过像...这样的逻辑来完成的.

return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);
Run Code Online (Sandbox Code Playgroud)

TryExecuteTask返回布尔值的原因是它处理同步以确保给定的任务只执行一次).因此,如果调度程序想要完全禁止在此期间内联任务Wait,则可以将其实现为return false; 如果调度程序希望始终允许内联,则可以将其实现为:

return TryExecuteTask(task);
Run Code Online (Sandbox Code Playgroud)

在当前的实现中(.NET 4和.NET 4.5,我个人并不希望这会改变),以ThreadPool为目标的默认调度程序允许内联当前线程是否为ThreadPool线程,如果该线程是一个先前排队的任务.

请注意,这里没有任意的重入,因为默认调度程序在等待任务时不会抽取任意线程...它只允许内联任务,当然任何内联任务依次决定去做.另请注意,Wait在某些条件下甚至不会询问调度程序,而是更喜欢阻止.例如,如果传入可取消的CancellationToken,或者传入非无限超时,它将不会尝试内联,因为它可能需要任意长的时间来内联任务的执行,这是全部或全部,这最终可能会大大延迟取消请求或超时.总的来说,TPL试图在浪费正在进行的Wait线程和重复使用该线程之间取得相当大的平衡.这种内联对于递归分治问题(例如QuickSort)非常重要,在这些问题中,您生成多个任务,然后等待它们全部完成.如果这样做没有内联,那么当你耗尽池中的所有线程以及它想要给你的任何未来线程时,你很快就会陷入僵局.

从不同的Wait,这也是(远程)可能是Task.Factory.StartNew呼叫最终可能执行的任务,然后有,当且仅当所使用的调度选择同步为QueueTask调用的一部分运行任务..NET内置的调度程序都不会这样做,我个人认为这对调度程序来说是一个糟糕的设计,但理论上它是可行的,例如:

protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
    return TryExecuteTask(task);
}
Run Code Online (Sandbox Code Playgroud)

其重载Task.Factory.StartNew不接受a TaskScheduler使用来自的调度程序TaskFactory,在Task.Factory目标的情况下TaskScheduler.Current.这意味着如果您Task.Factory.StartNew从排队到此神话的任务中调用RunSynchronouslyTaskScheduler,它也会排队RunSynchronouslyTaskScheduler,导致StartNew调用同步执行任务.如果您对这一点感到担心(例如,您正在实现一个库,并且您不知道将从何处调用它),您可以明确地传递TaskScheduler.DefaultStartNew调用,使用Task.Run(总是这样TaskScheduler.Default),或使用TaskFactory创建目标TaskScheduler.Default.


编辑:好的,看起来我完全错了,一个正在等待任务的线程可能被劫持.这是一个更简单的例子:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew(Launch).Wait();
            }
        }

        static void Launch()
        {
            Console.WriteLine("Launch thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
            Task.Factory.StartNew(Nested).Wait();
        }

        static void Nested()
        {
            Console.WriteLine("Nested thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

样本输出:

Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Run Code Online (Sandbox Code Playgroud)

如您所见,有很多次重用等待线程来执行新任务.即使线程已获得锁定,也可能发生这种情况.讨厌的重新入侵.我感到非常震惊和担心:(