Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bug in TPL - TaskContinuationOptions.ExecuteSynchronously?

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:

  1. I create a task that does simple thing. No exceptions etc.
  2. I register a continuation with ExecuteSynchronously set. It must be on the same thread.
  3. I start the task on default taskscheduler (ThreadPool). The starting thread proceeds and waits for it.
  4. The task starts. Passes.
  5. The continuation starts on the same thread as task (making previous task completed!) and enters endless loop.
  6. Nothing happens with waiting thread. Does not want to go further. Stuck on wait. I checked in debugger, task is RunToCompletion.

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. 
like image 976
Sławomir Nidecki Avatar asked May 27 '12 15:05

Sławomir Nidecki


2 Answers

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

like image 71
James Manning Avatar answered Oct 07 '22 07:10

James Manning


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.

like image 33
Ohad Schneider Avatar answered Oct 07 '22 05:10

Ohad Schneider