I think I spotted a serious bug in TPL. I am not sure. I spent a lot of time scratching my head and cannot understand the behavior. Can anyone help?
What my scenario is:
Here is my code. Appreciate any help!
// note: using new Task() and then Start() to avoid race condition dangerous
// with TaskContinuationOptions.ExecuteSynchronously flag set on continuation.
var task = new Task(() => { /* just return */ });
task.ContinueWith(
_task => { while (true) { } /* never return */ },
TaskContinuationOptions.ExecuteSynchronously);
task.Start(TaskScheduler.Default);
task.Wait(); // a thread hangs here forever even when EnterEndlessLoop is already called.
Filed a bug on connect on your behalf - hope that's ok :)
https://connect.microsoft.com/VisualStudio/feedback/details/744326/tpl-wait-call-on-task-doesnt-return-until-all-continuations-scheduled-with-executesynchronously-also-complete
I agree that it's a bug, just wanted to post a code snippet that shows the issue without having an 'endless' wait. The bug is that an ExecuteSynchronously means Wait calls on the first task don't return until the ExecuteSynchronously continuations are also completed.
Running the below snippet shows it waits 17 seconds (so all 3 had to complete instead of just the first one). It's the same whether the third task is scheduled off the first or the second (so this ExecuteSynchronously will continue through the tree of tasks scheduled as such).
void Main()
{
var task = new Task(() => Thread.Sleep(2 * 1000));
var secondTask = task.ContinueWith(
_ => Thread.Sleep(5 * 1000),
TaskContinuationOptions.ExecuteSynchronously);
var thirdTask = secondTask.ContinueWith(
_ => Thread.Sleep(10 * 1000),
TaskContinuationOptions.ExecuteSynchronously);
var stopwatch = Stopwatch.StartNew();
task.Start(TaskScheduler.Default);
task.Wait();
Console.WriteLine ("Wait returned after {0} seconds", stopwatch.ElapsedMilliseconds / 1000.0);
}
The only thing that makes me think it might be intentional (and therefore more a doc bug than code bug) is Stephen's comment in this blog post:
ExecuteSynchronously is a request for an optimization to run the continuation task on the same thread that completed the antecedent task off of which we continued, in effect running the continuation as part of the antecedent’s transition to a final state
This behavior makes sense when you consider task inlining. When you call Task.Wait
before the task has begun execution, the scheduler will attempt to inline it, i.e. run it on the same thread that called Task.Wait
. This makes sense - why waste a thread waiting for the task when you can reuse the thread in order to execute the task?
Now, when ExecuteSynchronously
is specified, the scheduler is instructed to execute the continuation on the same thread as the antecedent task - which is the original calling thread in the case of inlining.
Note that when inlining doesn't take place, the behavior you were expecting does take place. All you have to do is forbid inlining, and that's easy - either specify a timeout for the wait or pass a cancellation token, e.g.
task.Wait(new CancellationTokenSource().Token); //This won't wait for the continuation
Finally note that inlining is not guaranteed. On my machine, it didn't happen because the task has already begun before the Wait
call, so my Wait
call didn't block. If you want a reproducible block, call Task.RunSynchronously
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With