I posted Stephen Toub - a member of the PFX Team - about this issue. He comes back to me very quickly, with a lot of details, so I just copy and paste his text here. I haven’t quoted all of this, since reading a large amount of quoted text ends up becoming less convenient than vanilla black and white, but actually it’s Steven - I don’t know this much :) I made this answer from the community wiki to reflect that all the goodness below is not my content.
If you call Wait() in the completed task, there will be no lock (it simply throws an exception if the task completed in a state other than RanToCompletion , or otherwise returns as nop) If you call Wait() in the task that is already running, it should be blocked, because there is nothing that could reasonably be done (when I say “block”, I include both true expectation and kernel-based hangs, as it is usually a mixture of both). Similarly, if you call Wait() in a Task that is in the Created or WaitingForActivation , it blocks until the task completes. None of these are the interesting issues being discussed.
An interesting case is that you call Wait() in the Task in the WaitingToRun state, which means that it was previously queued for the TaskScheduler, but the TaskScheduler has not yet reached the actual execution of the Task delegate yet. In this case, the Wait call will ask the scheduler whether it is normal to run Task, and then in the current thread, by calling the scheduler method TryExecuteTaskInline . The scheduler can choose to either start the task then and there using the base.TryExecuteTask call, or it can return false to indicate that it is not performing the task (often this is done using logic like return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task); .. The reason TryExecuteTask returns a Boolean is because it handles the synchronization to ensure that the task runs only once). Thus, if the scheduler wants to completely prohibit nesting tasks while waiting, it can simply be implemented as return false; If the scheduler wants to always allow embedding when possible, it can simply be implemented as return TryExecuteTask(task); In the current implementation (both .NET 4 and .NET 4.5, and I personally do not expect this to change), the default scheduler that targets ThreadPool allows embedding if the current thread is a ThreadPool thread, and if that thread was those who previously put the task in line
Please note that there is no arbitrary re-inclusion here, since the default scheduler will not pump up arbitrary threads when waiting for a task ... it will only allow this task to be built-in and, of course, it decides to do any insertion of this task. Also note that Wait does not even ask the scheduler in certain conditions, instead prefers to block. For example, if you pass a canceled CancellationToken, or if you pass an infinite timeout, it will not try to embed it, because it may take as long as it takes to complete a task that is all or nothing, and that can lead to a significant delay in the cancellation request or timeout . All in all, TPL is trying to strike a decent balance between wasting a thread that is waiting and reusing that thread too much. Such an insertion is really important for recursive separation and victory problems, for example. QuickSort, where you run several tasks, and then wait for them to finish ... if it were done without inlay, you would very quickly get stuck when you ran out of all the threads in the pool and any future ones that he wanted to give you.
Apart from Wait, it is also possible (remotely) that a call to Task.Factory.StartNew can complete the task then and there if the scheduled scheduler decides to execute the task synchronously as part of the QueueTask call. None of the schedulers built into .NET will ever do this, and I personally think that this will be a bad design for the scheduler, but it is theoretically possible, for example. protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); } protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); } . Overloading Task.Factory.StartNew , which does not accept TaskScheduler, uses the scheduler from TaskFactory, which in the case of Task.Factory targets TaskScheduler.Current. This means that if you call Task.Factory.StartNew from a task queued for this mythical RunSynchronouslyTaskScheduler, it will also be queued for RunSynchronouslyTaskScheduler, as a result of which StartNew will execute the task synchronously. If you are generally worried about this (for example, you are implementing a library, and you do not know where you will be called from), you can explicitly pass TaskScheduler.Default to a StartNew call, use Task.Run (which always goes to TaskScheduler.Default) or use TaskFactory created for the target TaskScheduler.Default.
EDIT: Well, it looks like I was completely wrong, and the thread that is currently waiting for the job can be captured. Here is a simpler example of this:
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); } } }
Output Example:
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 many times when the wait thread is reused to perform a new task. This can happen even if the thread has acquired a lock. Unpleasant re-intervention. I'm shocked and worried enough :(