一尘不染

是否可以保证Task.Factory.StartNew()使用调用线程以外的其他线程?

c#

我正在从一个函数开始一个新任务,但我不希望它在同一线程上运行。我不在乎它在哪个线程上运行,只要它是另一个线程即可(因此,此问题中给出的信息无济于事)。

我是否保证下面的代码TestLockTask 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();
}

编辑: 根据乔恩·斯凯特(Jon Skeet)和斯蒂芬·图布(Stephen
Toub)的以下回答,一种确定性地防止重入的简单方法是传递一个CancellationToken,如以下扩展方法所示:

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

阅读 964

收藏
2020-05-19

共1个答案

一尘不染

我就此问题邮寄了PFX小组成员Stephen Toub
。他很快就很详细地回到了我身边,所以我将在这里复制并粘贴他的文字。我还没有全部引用,因为阅读大量引用的文本最终会变得不如香草黑与白舒适,但实际上,这是斯蒂芬-
我不知道这么多东西:)我做了这个答案社区Wiki反映出以下所有优点并不是我真正的内容:

如果您调用已完成Wait()Task,则不会有任何阻塞(如果任务以TaskStatus而不是来完成RanToCompletion,否则将抛出异常,否则将返回nop)。如果您调用Wait()已经执行的Task,则它必须阻塞,因为它无法合理地做其他事(当我说阻塞时,我同时包含了基于内核的真正的等待和自旋,因为它通常会同时混合两者)。同样,如果您调用状态Wait()Created或的任务,则WaitingForActivation该任务将阻塞直到任务完成。这些都不是正在讨论的有趣案例。

有趣的情况是,当您Wait()在该WaitingToRun状态下调用Task时,这意味着该任务先前已排队到TaskScheduler中,但该TaskScheduler尚未真正开始运行Task的委托。在这种情况下,Wait通过调用调度程序的TryExecuteTaskInline方法,对的调用将询问调度程序是否可以在当前线程上运行该任务。这称为
内联
。调度程序可以选择通过调用来内联任务base.TryExecuteTask,也可以返回’false’表示它没有执行任务(通常使用类似…的逻辑来完成)。

return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);

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

return TryExecuteTask(task);

在当前的实现中(.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);
}

那的重载Task.Factory.StartNew不能接受TaskScheduler来自的使用调度器TaskFactory,在Task.Factory目标的情况下TaskScheduler.Current。这意味着,如果您Task.Factory.StartNew从排队到这个神话的Task内调用RunSynchronouslyTaskScheduler,它也将排队到RunSynchronouslyTaskScheduler,导致该StartNew调用同步执行Task。如果您对此非常担心(例如,您正在实现一个库,但您不知道从何处调用),则可以显式传递TaskScheduler.Default给该StartNew调用,使用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);
        }
    }
}

样本输出:

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

如您所见,很多时候等待线程被重用于执行新任务。即使线程已获得锁,也会发生这种情况。讨厌的重新进入。我感到震惊和担心:(

2020-05-19