Reputation: 12424
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
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:
Task.Factory.StartNew
without passing a TaskScheduler
. This is a 100%-of-the-time, always-on rule.Task.Run
instead of Task.Factory.StartNew
.Upvotes: 6