76

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

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

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

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

4 回答 4

83

我就这个问题向PFX 团队的成员 Stephen Toub 发送了邮件。他很快就回来找我了,提供了很多细节——所以我就在这里复制并粘贴他的文字。我没有全部引用,因为阅读大量引用的文本最终会比普通的黑白黑白更舒服,但实际上,这是斯蒂芬 - 我不知道这么多东西 :) 我做了这个答案社区 wiki 以反映以下所有优点并不是我真正的内容:

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

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

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

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

return TryExecuteTask(task);

在当前实现中(.NET 4 和 .NET 4.5,我个人不希望这会改变),如果当前线程是 ThreadPool 线程并且该线程是一个先前已将任务排队。

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

与 分开,如果正在使用的调度程序选择作为 QueueTask 调用的一部分同步运行任务Wait,则Task.Factory.StartNew调用也有可能最终执行任务。.NET 中内置的调度程序都不会这样做,我个人认为这对调度程序来说是一个糟糕的设计,但理论上是可能的,例如:

protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
    return TryExecuteTask(task);
}

that的重载Task.Factory.StartNew不接受 aTaskScheduler使用来自的调度程序,在targetsTaskFactory的情况下。这意味着如果您从排队到这个神话的任务中调用,它也会排队到,导致调用同步执行任务。如果您对此完全担心(例如,您正在实现一个库并且您不知道将从哪里调用您),您可以显式传递给调用,使用(总是转到),或使用created 来定位.Task.FactoryTaskScheduler.CurrentTask.Factory.StartNewRunSynchronouslyTaskSchedulerRunSynchronouslyTaskSchedulerStartNewTaskScheduler.DefaultStartNewTask.RunTaskScheduler.DefaultTaskFactoryTaskScheduler.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

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

于 2012-09-03T10:26:31.243 回答
4

为什么不只是为它设计,而不是向后弯腰以确保它不会发生?

TPL 在这里是一条红鲱鱼,只要您可以创建一个循环,任何代码都可能发生重入,并且您不确定堆栈帧的“南部”会发生什么。同步重入是最好的结果——至少你不能自己(很容易)自死锁。

锁管理跨线程同步。它们与管理重入是正交的。除非您要保护真正的一次性资源(可能是物理设备,在这种情况下您可能应该使用队列),否则为什么不只是确保您的实例状态是一致的,以便重入可以“正常工作”。

(边想:信号量是否可重入而不递减?)

于 2012-09-04T13:22:33.587 回答
0

您可以通过编写一个在线程/任务之间共享套接字连接的快速应用程序来轻松测试这一点。

该任务将在通过套接字发送消息并等待响应之前获取锁。一旦这个块并变得空闲(IOBlock),在同一个块中设置另一个任务来做同样的事情。它应该阻止获取锁,如果它没有,并且第二个任务被允许通过锁,因为它由同一个线程运行,那么你就有问题了。

于 2012-09-04T07:54:19.683 回答
0

Erwin提出的解决方案new CancellationToken()对我不起作用,无论如何都发生了内联。

所以我最终使用了 Jon 和 Stephen ( ... or if you pass in a non-infinite timeout ...) 建议的另一个条件:

  Task<TResult> task = Task.Run(func);
  task.Wait(TimeSpan.FromHours(1)); // Whatever is enough for task to start
  return task.Result;

注意:为简单起见,此处省略异常处理等,您应该注意生产代码中的那些。

于 2016-09-01T07:09:58.650 回答