Search code examples
c#.netasync-awaittask-parallel-library

Why ExecuteSynchronously is not the default for async await?


We know that if we uses async/await

await methodAsync();
var result = methodNonAsync()
// ...

await methodAsync() puts a item on the queue, so that a thread pool thread (says threadA) can execute this task.

and I would say it is optimized that threadA also execute var result = methodNonAsync().

But when I check the source code https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs,0fb2b4d9262599b9 of ContinueWith (that's how async/await works behind the scene)

public Task ContinueWith(Action<Task> continuationAction)
{
    return ContinueWith(continuationAction, TaskScheduler.Current, default, TaskContinuationOptions.None);
}

it uses TaskContinuationOptions.None not TaskContinuationOptions.ExecuteSynchronously, so it means another threadB from thread pool might executes `var result = methodNonAsync(), so this is an extra context switch.

so why ExecuteSynchronously is not the default option for async await usage for performance boost?


Solution

  • So, for starters, await, as a language construct, has no knowledge of any sort of continuation options or anything like that. await, from the language's perspective, will call GetAwaiter on the awaited object, and then call IsCompleted, and if it's not completed, it'll call OnCompleted. Thus you can make an awaitable object do anything you want. You can make an awaitable object that always constructs a new Thread and runs the continuations there, or that throws the action away and never runs it, or uses your own custom thread pool (and not the .NET ThreadPool). You can make a continuation that always executes all continuations synchronously on being marked as completed (however you want to allow it to even be marked as completed).

    So onto what your code actually does, we can look at Task.GetAwaiter specifically, which returns TaskAwaiter (note in previous C# versions it wasn't even a public implementation, it was an internal class). TaskAwaiter.OnCompleted doesn't technically call Task.ContinueWith, contrary to your statement in the question (note that the implementation details of the awaiter have changed over different versions, there might be one where it does). It does a functionally similar thing in that it stores the delegate to be executed when the task is finished, but relevantly, none of the internal methods called actually store TaskContinuationOptions, None or otherwise.

    TaskAwaiter's implementation first needs to consider if the current synchronization context is captured, (the default behavior for tasks) which, if it needs to, prevents the execution from being run synchronously, it forces a call to the synchronization context. Even outside that case, there are a whole bunch of other things to consider (the settings of the TaskCompletionSource the task comes from, the current context/scheduler, etc.) but it is possible for continuations to be inlined in certain situations and run synchronously as the task is marked as completed.

    If the behavior of the task awaiter is unacceptable for your specific situation (recognizing that the awaiter's behavior is potentially configurable in some ways) you always have the last resort of wrapping the object in your own awaiter that does whatever you want to add the continuation. This could possibly be implemented by adding your own extension method to Task that constructs your own awaiter (even if it's just calling ContinueWith on said task with TaskContinuationOptions.ExecuteSynchronously, using a custom task scheduler, etc.).