Reputation: 5146
In a blog post, Microsoft's Sergey Tepliakov says:
you should always provide TaskCreationOptions.RunContinuationsAsynchronously when creating TaskCompletionSource instances.
But in that article, unless I misunderstood it, he also says that all await
continuations basically act the same way as a TaskCompletionSource
without TaskCreationOptions.RunContinuationsAsynchronously
. So this would mean that async
and await
are also inherently dangerous and shouldn't be used, unless await Task.Yield()
is used as well.
(edit: I did misunderstand it. He's saying that await
continuing synchronously is the cause of the problematic SetResult
behaviour. await Task.Yield()
is recommended as a client-side workaround if the task creation can't be controlled at the source. Unless I misunderstood something again.)
I conclude that there must be specific circumstances that make synchronous continuations dangerous, and apparently that those circumstances are common for TaskCompletionSource
use cases. What are those dangerous circumstances?
Upvotes: 4
Views: 373
Reputation: 1063013
Thread theft, basically.
A good example here would be a client library that talks over the network to some server that is implemented as a sequence of frames on the same connection (http/2, for example, or RESP). For whatever reason, imagine that each call to the library returns a Task<T>
(for some <T>
) that has been provided by a TaskCompletionSource<T>
that is awaiting results from the network server.
Now you want to write the read-loop that is dequeing responses from the server (presumably from a socket, stream, or pipeline API), resolve the corresponding TaskCompletionSource<T>
(which could mean "the next in the queue", or could mean "use a unique correlation key/token that is present in the response").
So you effectively have:
while (communicationIsAlive)
{
var result = await ParseNextResultAsync(); // next message from the server
TaskCompletionSource<Foo> tcs = TryResolveCorrespondingPendingRequest(result);
tcs?.TrySetResult(result); // or possibly TrySetException, etc
}
Now; imagine that you didn't use RunContinuationsAsynchronously
when you created the TaskCompletionSource<T>
. The moment you do TrySetResult
, the continuation executes on the current thread. This means that the await
in some arbitrary client code (which can do anything) has now interrupted your IO read loop, and no other results will be processed until that thread relinquishes the thread.
Using RunContinuationsAsynchronously
allows you to use TrySetResult
(et al) without worrying about your thread being stolen.
(even worse: if you combine this with a subsequent sync-over-async call to the same resource, you can cause a hard deadlock)
Related question from before this flag existed: How can I prevent synchronous continuations on a Task?.
Upvotes: 7