CodeMonkey
CodeMonkey

Reputation: 12424

What happens when awaiting a call on a different thread

Let's say I invoke a Task like this:

Task.Factory.StartNew(async () =>
                {
                    await SomeAction();
                }, TaskCreationOptions.LongRunning);

and inside SomeAction there's a line like this:

await OtherAction();

with a regular await, the code goes back to the invoking context, but here it's a task running on a different thread. So what happens with the context of the await there? Is it actually like blocking in this case?

Upvotes: 1

Views: 105

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 456407

with a regular await, the code goes back to the invoking context, but here it's a task running on a different thread. So what happens with the context of the await there? Is it actually like blocking in this case?

Here's how await works. await never blocks.

await will first examine its awaitable (usually a task); if the awaitable is already complete, then await will extract the result (or exception) and just continue executing the method (synchronously). If the awaitable is not already complete, then await will behave asynchronously.

When await behaves asynchronously, it will (by default) capture the current "context" (SynchronizationContext.Current or, if that is null, then TaskScheduler.Current), and then return an incomplete task. Since the example code is using Task.Factory.StartNew without specifying a TaskScheduler (which IMO is always a bug), then its delegate was executed on whatever TaskScheduler.Current was at the point StartNew was called. So the context that will be captured by the await is that same TaskScheduler.Current.

If TaskScheduler.Current was the same as TaskScheduler.Default (which is likely but by no means guaranteed), then when the async method returns, the thread is returned to the thread pool, and since StartNew doesn't understand async lambdas, the Task returned from StartNew is completed. This means the LongRunning flag in there just makes things slower; it's the opposite of optimization.

Later, when the await completes, it will resume executing the lambda on the captured context (TaskScheduler.Current). If TaskScheduler.Current was the same as TaskScheduler.Default, then the lambda will resume executing on a thread newly acquired from the thread pool (possibly the same thread but likely a different one).


This sounds crazy complicated, but really that's only because the code is using StartNew. If you look at the code using Task.Run, it's much simpler because the context is always TaskScheduler.Default (the thread pool context):

Task.Run(async () =>
{
  await SomeAction();
});

Now you can say with certainty that if the await acts asynchronously, then it will return a task, returning the thread to the thread pool. When SomeAction completes, then the lambda will continue executing on a thread pool thread. Also, since Task.Run understands async lambdas, the task returned from Task.Run will not complete until the lambda is complete.

Here's some guidelines:

Upvotes: 6

Related Questions