的成员 Stephen Toub 发了一封邮件询问这个问题。他非常迅速地回复了我,并提供了大量详细信息 - 所以我将在此处直接复制粘贴他的文字。由于阅读大量引用文本比起普通黑白色更加不舒适,因此我没有全部引用。但是,这确实是 Stephen - 我不知道这么多 :) 我将此答案设为社区 wiki,以反映下面所有好东西并不是我的内容:
If you call Wait()
on a Task that's completed, there won't be any blocking (it'll just throw an exception if the task completed with a TaskStatus other than RanToCompletion
, or otherwise return as a nop). If you call Wait()
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 call Wait()
on a Task that has the Created
or WaitingForActivation
status, 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 the WaitingToRun
state, meaning that it’s previously been queued to a TaskScheduler but that TaskScheduler hasn't yet gotten around to actually running the Task's delegate yet. In that case, the call to Wait
will ask the scheduler whether it's ok to run the Task then-and-there on the current thread, via a call to the scheduler's TryExecuteTaskInline
method. This is called inlining. The scheduler can choose to either inline the task via a call to base.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 TryExecuteTask
returns 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 during Wait
, it can just be implemented as return 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 Wait
won’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 the Wait
'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.StartNew call 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.StartNew
that doesn’t accept a TaskScheduler
uses the scheduler from the TaskFactory
, which in the case of Task.Factory
targets TaskScheduler.Current
. This means if you call Task.Factory.StartNew
from within a Task queued to this mythical RunSynchronouslyTaskScheduler
, it would also queue to RunSynchronouslyTaskScheduler
, resulting in the StartNew
call 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 pass TaskScheduler.Default
to the StartNew
call, use Task.Run
(which always goes to TaskScheduler.Default
), or use a TaskFactory
created to target TaskScheduler.Default
.
Task.Factory.StartNew(() => { this.Test(stop: true); }, TaskCreationOptions.LongRunning);
如果您的锁可能会使线程进入长时间等待状态,那么这是一个不错的选择。 - Peter Ritchie