我就此问题邮寄了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
如您所见,很多时候等待线程被重用于执行新任务。即使线程已获得锁,也会发生这种情况。讨厌的重新进入。我感到震惊和担心:(
StartNew
。任务定义为异步操作,它不一定意味着新线程。也可以在某个地方使用现有线程,或者使用另一种异步方式。