It's All About the SynchronizationContext (and ConfigureAwait
)
The behaviour will depend on the current SynchronizationContext:
The purpose of the synchronization model implemented by this class is to allow the internal asynchronous/synchronous operations of the common language runtime to behave properly with different synchronization models.
ConfigureAwait
With ConfigureAwait
you can affect the continuation behaviour.
Parameters
continueOnCapturedContext
Boolean
true
to attempt to marshal the continuation back to the original context captured; otherwise, false
.
WPF
Your example is likely from a WPF app and will use DispatcherSynchronizationContext
. Here is a passage from
Parallel Computing - It's All About the SynchronizationContext about it.
DispatcherSynchronizationContext (WindowsBase.dll: System.Windows.Threading) WPF and Silverlight applications use a DispatcherSynchronizationContext, which queues delegates to the UI thread’s Dispatcher with “Normal” priority. This SynchronizationContext is installed as the current context when a thread begins its Dispatcher loop by calling Dispatcher.Run. The context for DispatcherSynchronizationContext is a single UI thread.
All delegates queued to the DispatcherSynchronizationContext are executed one at a time by a specific UI thread in the order they were queued. The current implementation creates one DispatcherSynchronizationContext for each top-level window, even if they all share the same underlying Dispatcher.
Examples
1. WPF, no .ConfigureAwait
In the context of a WPF app with no .ConfigureAwait(false)
SynchronousCode2
would be executed in the same thread.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Print($"SynchronizationContext.Current: {SynchronizationContext.Current}");
Print("\t button_push START");
genericMethodAsync();
Print("\t button_push END");
}
public async void genericMethodAsync()
{
Print("\t\t genericMethodAsync START");
await someOtherAsyncMethod();
Print("\t\t genericMethodAsync calling SynchronousCode2");
SynchronousCode2();
Print("\t\t genericMethodAsync END");
}
private void SynchronousCode2()
{
Print("\t\t\t SynchronousCode2 START");
Print("\t\t\t SynchronousCode2 END");
}
private async Task someOtherAsyncMethod()
{
Print("\t\t\t someOtherAsyncMethod START");
await Task.Delay(TimeSpan.FromSeconds(2));
Print("\t\t\t someOtherAsyncMethod END");
}
private static void Print(string v) =>
Console.WriteLine($"T{System.Threading.Thread.CurrentThread.ManagedThreadId}: {v}");
}
T1: SynchronizationContext.Current: System.Windows.Threading.DispatcherSynchronizationContext
T1: button_push START
T1: genericMethodAsync START
T1: someOtherAsyncMethod START
T1: button_push END
T1: someOtherAsyncMethod END
T1: genericMethodAsync calling SynchronousCode2
T1: SynchronousCode2 START
T1: SynchronousCode2 END
T1: genericMethodAsync END
3. WPF, with .ConfigureAwait
If ConfigureAwait(false)
is used, then SynchronousCode2
may be called by another thread.
await someOtherAsyncMethod().ConfigureAwait(continueOnCapturedContext: false);
Output
T1: SynchronizationContext.Current: System.Windows.Threading.DispatcherSynchronizationContext
T1: button_push START
T1: genericMethodAsync START
T1: someOtherAsyncMethod START
T1: button_push END
T1: someOtherAsyncMethod END
T9: genericMethodAsync calling SynchronousCode2
T9: SynchronousCode2 START
T9: SynchronousCode2 END
T9: genericMethodAsync END
3. No context
In a context of a console application. The result will be different. SynchronousCode2
may or may not be executed by the same thread.
public void button_push()
{
Print("\t button_push START");
genericMethodAsync();
Print("\t button_push END");
}
public async void genericMethodAsync()
{
Print("\t\t genericMethodAsync START");
await someOtherAsyncMethod();
Print("\t\t genericMethodAsync calling SynchronousCode2");
SynchronousCode2();
Print("\t\t genericMethodAsync END");
}
private void SynchronousCode2()
{
Print("\t\t\t SynchronousCode2 START");
Print("\t\t\t SynchronousCode2 END");
}
private async Task someOtherAsyncMethod()
{
Print("\t\t\t someOtherAsyncMethod START");
await Task.Delay(TimeSpan.FromSeconds(2));
Print("\t\t\t someOtherAsyncMethod END");
}
private static void Print(string v) =>
Console.WriteLine($"T{System.Threading.Thread.CurrentThread.ManagedThreadId}: {v}");
static void Main(string[] args)
{
Print(" Main START");
new Program().button_push();
Print(" Main after button_push");
Console.ReadLine();
}
// .NETCoreApp,Version=v3.0
T1: Main START
T1: SynchronizationContext.Current: null
T1: button_push START
T1: genericMethodAsync START
T1: someOtherAsyncMethod START
T1: button_push END
T1: Main after button_push
T4: someOtherAsyncMethod END
T4: genericMethodAsync calling SynchronousCode2
T4: SynchronousCode2 START
T4: SynchronousCode2 END
T4: genericMethodAsync END
Note on async avoid
Generally speaking async void
should be avoided but event handlers are the exception.
The other very important rule is not to block the UI thread.
You could make button_push
async
, make genericMethodAsync
return Task
and make things more predictable.
public async void button_push()
{
await genericMethodAsync();
SynchronousCode1();
}
async Task genericMethodAsync()
{
await someOtherAsyncMethodAsync();
SynchronousCode2();
SyncronousCode3();
}