Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In which case does TaskCompletionSource.SetResult() run the continuation synchronously?

Initially I thought that all continuations are executed on the threadpool (given a default synchronization context). This however doesn't seem to be the case when I use a TaskCompletionSource.

My code looks something like this:

Task<int> Foo() {
  _tcs = new TaskCompletionSource<int>();
  return _tcs.Task;
}

async void Bar() {
  Console.WriteLine(Thread.Current.ManagedThreadId);
  Console.WriteLine($"{Thread.Current.ManagedThreadId} - {await Foo()}");
}

Bar gets called on a specific thread and the TaskCompletionSource stays unset for some time, meaning the returned tasks IsComplete = false. Then after some time, the same thread would proceed to call _tcs.SetResult(x), which by my understanding should run the continuation on the threadpool.

But what I observed in my application is that the thread running the continuation is in fact still the same thread, as if the continuation was invoked synchronously right as SetResult is called.

I even tried setting a breakpoint on the SetResult and stepping over it (and having a breakpoint in the continuation), which in turn actually goes on to call the continuation synchronously.

When exactly does SetResult() immediately call the continuation synchronously?

like image 400
Jakub Arnold Avatar asked Sep 02 '16 16:09

Jakub Arnold


2 Answers

Initially I thought that all continuations are executed on the threadpool (given a default synchronization context). This however doesn't seem to be the case when I use a TaskCompletionSource.

Actually, when using await, most continuations are executed synchronously.

Marc's answer is great; I just wanted to go into a bit more detail...

TaskCompletionSource<T> by default will operate synchronously when Set* is called. Set* will complete the task and issue the continuations in a single method call. (This means that calling Set* while holding a lock is a recipe for deadlocks.)

I use the weird phrase "issue the continuations" there because it may or may not actually execute them; more on that later.

The TaskCreationOptions.RunContinuationsAsynchronously flag will tell TaskCompletionSource<T> to issue the continuations asynchronously. This breaks apart the completing of the task (which is still done immediately by Set*) from the issuing of the continuations (which is only triggered by the Set* call). So with RunContinuationsAsynchronously, a Set* call will only complete the task; it will not execute the continuations synchronously. (This means that calling Set* while holding a lock is safe.)

But back to the default case, which issues the continuations synchronously.

Each continuation also has a flag; by default a continuation is executed asynchronously, but it can be made synchronous by TaskContinuationOptions.ExecuteSynchronously. (Note that await does use this flag - link is to my blog; technically this is an implementation detail and not officially documented).

However, even if ExecuteSynchronously is specified, there are a number of situations where the continuation is not executed synchronously:

  • If there is a TaskScheduler associated with the continuation, that scheduler is given the option of rejecting the current thread, in which case the task is queued to that TaskScheduler instead of executing synchronously.
  • If the current thread is being aborted, then the task is queued elsewhere.
  • If the current thread's stack is too deep, then the task is queued elsewhere. (This is only a heuristic, and not guaranteed to avoid StackOverflowException).

That's quite a few conditions, but with your simple Console app test, they're all met:

  • TaskCompletionSource<T> does not specify RunContinuationsAsynchronously.
  • The continuation (await) does specify ExecuteSynchronously.
  • The continuation does not have a TaskScheduler specified.
  • The target thread is able to execute the continuation (not being aborted; stack is ok).

As a general rule, I would say any usage of TaskCompletionSource<T> should specify TaskCreationOptions.RunContinuationsAsynchronously. Personally, I think the semantics are more appropriate and less surprising with that flag.

like image 138
Stephen Cleary Avatar answered Oct 22 '22 15:10

Stephen Cleary


SetResult usually runs continuations from TCS synchronously. There main exception to this is if you explicitly pass in the TaskContinuationOptions.RunContinuationsAsynchronously flag when creating the TCS (new in .NET 4.6). The other scenario when it runs things asynchronously is if it thinks the current thread is doomed.

This is quite important, because if you're not careful you can end up having calling code take control of a thread that was meant to be doing other work (like: dealing with socket IO).

like image 13
Marc Gravell Avatar answered Oct 22 '22 16:10

Marc Gravell