C# Task.Factory.StartNew() 是否保证使用另一个线程而不是调用线程?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/12245935/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Is Task.Factory.StartNew() guaranteed to use another thread than the calling thread?
提问by Erwin Mayer
I am starting a new task from a function but I would not want it to run on the same thread. I don't care which thread it runs on as long as it is a different one (so the information given in this questiondoes not help).
我正在从一个函数开始一个新任务,但我不希望它在同一个线程上运行。我不在乎它运行在哪个线程上,只要它是一个不同的线程(所以这个问题中给出的信息没有帮助)。
Am I guaranteed that the below code will always exit TestLockbefore allowing Task tto enter it again? If not, what is the recommended design pattern to prevent re-entrency?
我能保证下面的代码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();
}
Edit:Based on the below answer by Jon Skeet and Stephen Toub, a simple way to deterministically prevent reentrancy would be to pass a CancellationToken, as illustrated in this extension method:
编辑:根据 Jon Skeet 和 Stephen Toub 的以下回答,确定性地防止重入的一种简单方法是传递 CancellationToken,如此扩展方法所示:
public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action)
{
return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}
采纳答案by Jon Skeet
I mailed Stephen Toub - a member of the PFX Team- about this question. He's come back to me really quickly, with a lot of detail - so I'll just copy and paste his text here. I haven't quoted it all, as reading a large amount of quoted text ends up getting less comfortable than vanilla black-on-white, but really, this is Stephen - I don't know this much stuff :) I've made this answer community wiki to reflect that all the goodness below isn't really my content:
我就这个问题给PFX 团队的成员 Stephen Toub 发了邮件。他很快就回复了我,提供了很多细节——所以我将他的文字复制并粘贴到这里。我还没有全部引用,因为阅读大量引用的文本最终比香草黑白更不舒服,但实际上,这是斯蒂芬 - 我不知道这么多东西:) 我已经做了这个答案社区维基反映了下面的所有优点都不是我的内容:
If you call
Wait()on a Taskthat's completed, there won't be any blocking (it'll just throw an exception if the task completed with a TaskStatusother thanRanToCompletion, or otherwise return as a nop). If you callWait()on a Task that's already executing, it must block as there's nothing else it can reasonably do (when I say block, I'm including both true kernel-based waiting and spinning, as it'll typically do a mixture of both). Similarly, if you callWait()on a Task that has theCreatedorWaitingForActivationstatus, it'll block until the task has completed. None of those is the interesting case being discussed.The interesting case is when you call
Wait()on a Task in theWaitingToRunstate, meaning that it's previously been queued to a TaskSchedulerbut that TaskScheduler hasn't yet gotten around to actually running the Task's delegate yet. In that case, the call toWaitwill ask the scheduler whether it's ok to run the Task then-and-there on the current thread, via a call to the scheduler'sTryExecuteTaskInlinemethod. This is called inlining. The scheduler can choose to either inline the task via a call tobase.TryExecuteTask, or it can return 'false' to indicate that it is not executing the task (often this is done with logic like...return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);The reason
TryExecuteTaskreturns a Boolean is that it handles the synchronization to ensure a given Task is only ever executed once). So, if a scheduler wants to completely prohibit inlining of the Task duringWait, it can just be implemented asreturn false;If a scheduler wants to always allow inlining whenever possible, it can just be implemented as:return TryExecuteTask(task);In the current implementation (both .NET 4 and .NET 4.5, and I don't personally expect this to change), the default scheduler that targets the ThreadPool allows for inlining if the current thread is a ThreadPool thread and if that thread was the one to have previously queued the task.
Note that there isn't arbitrary reentrancy here, in that the default scheduler won't pump arbitrary threads when waiting for a task... it'll only allow that task to be inlined, and of course any inlining that task in turn decides to do. Also note that
Waitwon't even ask the scheduler in certain conditions, instead preferring to block. For example, if you pass in a cancelable CancellationToken, or if you pass in a non-infinite timeout, it won't try to inline because it could take an arbitrarily long amount of time to inline the task's execution, which is all or nothing, and that could end up significantly delaying the cancellation request or timeout. Overall, TPL tries to strike a decent balance here between wasting the thread that's doing theWait'ing and reusing that thread for too much. This kind of inlining is really important for recursive divide-and-conquer problems (e.g. QuickSort) where you spawn multiple tasks and then wait for them all to complete. If such were done without inlining, you'd very quickly deadlock as you exhaust all threads in the pool and any future ones it wanted to give to you.Separate from
Wait, it's also (remotely) possible that the Task.Factory.StartNewcall could end up executing the task then and there, iff the scheduler being used chose to run the task synchronously as part of the QueueTask call. None of the schedulers built into .NET will ever do this, and I personally think it would be a bad design for scheduler, but it's theoretically possible, e.g.:protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); }The overload of
Task.Factory.StartNewthat doesn't accept aTaskScheduleruses the scheduler from theTaskFactory, which in the case ofTask.FactorytargetsTaskScheduler.Current. This means if you callTask.Factory.StartNewfrom within a Task queued to this mythicalRunSynchronouslyTaskScheduler, it would also queue toRunSynchronouslyTaskScheduler, resulting in theStartNewcall executing the Task synchronously. If you're at all concerned about this (e.g. you're implementing a library and you don't know where you're going to be called from), you can explicitly passTaskScheduler.Defaultto theStartNewcall, useTask.Run(which always goes toTaskScheduler.Default), or use aTaskFactorycreated to targetTaskScheduler.Default.
如果您调用已完成
Wait()的任务,则不会有任何阻塞(如果任务完成时的TaskStatus不是RanToCompletion,它只会抛出异常,否则返回为nop)。如果你调用Wait()一个已经在执行的任务,它必须阻塞,因为它不能合理地做任何其他事情(当我说阻塞时,我包括真正的基于内核的等待和旋转,因为它通常会混合两者)。同样,如果您调用Wait()具有Created或WaitingForActivation状态的任务,它将阻塞直到任务完成。这些都不是正在讨论的有趣案例。有趣的情况是当您调用
Wait()状态中的 Task 时WaitingToRun,这意味着它之前已排队到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)非常重要,在这种问题中,您生成多个任务,然后等待它们全部完成。如果这样做没有内联,那么当您耗尽池中的所有线程以及它想要给您的任何未来线程时,您将很快陷入僵局。与 分开
Wait,Task.Factory.StartNew调用也有可能(远程)最终执行任务,如果使用的调度程序选择同步运行任务作为 QueueTask 调用的一部分。.NET 中内置的调度程序都不会这样做,我个人认为这对调度程序来说是一个糟糕的设计,但理论上是可能的,例如:protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); }的重载
Task.Factory.StartNew不接受 aTaskScheduler使用来自 的调度程序TaskFactory,在Task.Factory目标的情况下TaskScheduler.Current。这意味着如果您Task.Factory.StartNew从排队到这个 mythical 的 Task 中调用RunSynchronouslyTaskScheduler,它也会排队到RunSynchronouslyTaskScheduler,从而导致StartNew调用同步执行 Task。如果您对此完全担心(例如,您正在实现一个库并且您不知道将从哪里调用您),您可以明确地传递TaskScheduler.Default给StartNew调用,使用Task.Run(总是转到TaskScheduler.Default),或使用TaskFactorycreated 来定位TaskScheduler.Default.
EDIT: Okay, it looks like I was completely wrong, and a thread which is currently waiting on a task canbe hiHymaned. Here's a simpler example of this happening:
编辑:好的,看起来我完全错了,当前正在等待任务的线程可以被劫持。这是发生这种情况的一个更简单的例子:
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);
}
}
}
Sample output:
示例输出:
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
As you can see, there are lots of times when the waiting thread is reused to execute the new task. This can happen even if the thread has acquired a lock. Nasty re-entrancy. I am suitably shocked and worried :(
如您所见,有很多次等待线程被重用来执行新任务。即使线程获得了锁,这也可能发生。讨厌的重入。我感到非常震惊和担心:(
回答by JonPen
You could easily test this by writting a quick app that shared a socket connection between threads / tasks.
您可以通过编写一个在线程/任务之间共享套接字连接的快速应用程序来轻松测试。
The task would acquire a lock before sending a message down the socket and waiting for a response. Once this blocks and becomes idle (IOBlock) set another task in the same block to do the same. It should block on acquiring the lock, if it does not and the second task is allowed to pass the lock because it run by the same thread then you have an problem.
该任务将在通过套接字发送消息并等待响应之前获取锁。一旦此阻塞并变为空闲(IOBlock),则在同一块中设置另一个任务以执行相同的操作。它应该在获取锁时阻塞,如果它没有并且允许第二个任务通过锁,因为它由同一个线程运行,那么你就有问题了。
回答by piers7
Why not just design for it, rather than bend over backwards to ensure it doesn't happen?
为什么不只是为它设计,而不是向后弯曲以确保它不会发生?
The TPL is a red herring here, reentrancy can happen in any code provided you can create a cycle, and you don't know for sure what's going to happen 'south' of your stack frame. Synchronous reentrancy is the best outcome here - at least you can't self-deadlock yourself (as easily).
TPL 在这里是一个红鲱鱼,只要您可以创建循环,任何代码中都可能发生重入,并且您不确定堆栈帧的“南”处会发生什么。同步重入是这里最好的结果 - 至少你不能自我死锁(那么容易)。
Locks manage cross thread synchronisation. They are orthogonal to managing reentrancy. Unless you are protecting a genuine single use resource (probably a physical device, in which case you should probably use a queue), why not just ensure your instance state is consistent so reentrancy can 'just work'.
锁管理跨线程同步。它们与管理可重入性是正交的。除非您正在保护真正的一次性资源(可能是物理设备,在这种情况下您可能应该使用队列),否则为什么不确保您的实例状态一致,以便可重入可以“正常工作”。
(Side thought: are Semaphores reentrant without decrementing?)
(侧面思考:信号量是否可重入而不递减?)
回答by Vitaliy Ulantikov
Solution with new CancellationToken()proposed by Erwin did not work for me, inlining happened to occur anyway.
new CancellationToken()Erwin 提出的解决方案对我不起作用,无论如何都发生了内联。
So I ended up using another condition advised by Jon and Stephen
(... or if you pass in a non-infinite timeout ...):
所以我最终使用了 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;
Note:Omitting exception handling etc here for simplicity, you should mind those in production code.
注意:为了简单起见,这里省略了异常处理等,您应该注意生产代码中的那些。

