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

时间:2012-09-03 10:19:37

标签: c# multithreading locking task task-parallel-library

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

我是否保证在允许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 个答案:

答案 0 :(得分:82)

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

  

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

     

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

base.TryExecuteTask
     

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

return false;
     

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

     

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

     

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

Wait
     

不接受protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); } 的{​​{1}}超载使用Task.Factory.StartNew中的调度程序,TaskScheduler定位TaskFactory。这意味着如果您从排队到此神话Task.Factory的任务中调用TaskScheduler.Current,它也会排队到Task.Factory.StartNew,从而导致同步执行任务的RunSynchronouslyTaskScheduler调用。如果您对此感到担忧(例如,您正在实施一个库而您不知道将从何处调用),则可以明确地将RunSynchronouslyTaskScheduler传递给StartNew致电,使用TaskScheduler.Default(始终转到StartNew),或使用Task.Run创建定位TaskScheduler.Default


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

TaskFactory

示例输出:

TaskScheduler.Default

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

答案 1 :(得分:4)

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

TPL在这里是一个红色的鲱鱼,只要你可以创建一个循环就可以在任何代码中进行重入,并且你不确定你的堆栈帧的“南方”会发生什么。同步重入是最好的结果 - 至少你不能自我陷入困境(很容易)。

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

(Side想:Semaphores是否可以折返而不会减少?)

答案 2 :(得分:0)

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

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

答案 3 :(得分: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;

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