1
public void button_push()
{
    genericMethodAsync();
    SynchronousCode1();
}

public async void genericMethodAsync()
{
     await someOtherAsyncMethod();
     SynchronousCode2();
     SyncronousCode3();
}

Let's say I have a button that calls an async method, but doesn't await it. However, the async method that the button calls does have an await.
I assume that because the button click doesn't await the method, SynchronousCode1() may execute before SynchronousCode2().

Let's say the async method that is awaited takes a long time, like 2 seconds, and is an IO bound task. Will the SynchronousCode2() method finish on the same thread (or should I say same context) that that the button_push() method was called on?
I wasn't sure about this since the button_Push() command may run to completion before the await is finished in the async method.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
RomanOwensby
  • 91
  • 1
  • 2
  • 9
  • `void button_push` does not involve async code, so it has not reason to switch threads. `void genericMethodAsync` contains an `await` which captures the context, so `SynchronousCode2` should execute on that context. Thus, `SynchronousCode1` and `SynchronousCode2` should run on the same context/thread. That is no reason to do it this way though. – GSerg Nov 12 '19 at 23:26
  • 1
    You can't get any warranty for this kind of code. The detail that everybody *likes* to overlook, but never should, is what will happen when the user closes the window before those two seconds expire. Or presses the button again. – Hans Passant Nov 12 '19 at 23:28
  • @Hans: _"what will happen when the user closes the window"_ -- that only matters if the window that was closed causes the thread's message loop to terminate. Many windows won't, and getting into a discussion about when and how that might happen would unnecessarily complicate the discussion. I think it's reasonable for the sake of simplicity to "overlook" issues like that. – Peter Duniho Nov 12 '19 at 23:30
  • I understand that this is a terrible way to do it. I’ve read all the practices that are considered the “right” way to use async/await. I am more or less trying to understand what is happening in various scenarios, despite if they involve good or bad practices. Thanks for your comment! – RomanOwensby Nov 12 '19 at 23:33

3 Answers3

3

I assume that because the button click doesn't await the method, SynchronousCode1() may execute before SynchronousCode2().

Correct

Let's say the async method that is awaited takes a long time, like 2 seconds, and is an IO bound task. Will the SynchronousCode2() method finish on the same thread (or should I say same context) that that the button_push() method was called on?

Everything within the button_push method will execute on the same thread. Because it never uses await, it never yields control. All it does is post a new task onto a queue then move on to executing the rest of its code.

As for SynchronousCode2(), yes, there is a chance it will execute on a different thread or different context. It depends completely on how the synchronization context works. In the case of a console application (which has no synchronization context other than the thread pool) it could very likely be on a different thread. In an ASP.NET application running on the .NET framework, it will be on the same thread (subject to thread agility) and the same context. In an .NET Core application it could be different. In a WinForms app (where the synchronization is provided by the default message pump) it will be the same.

John Wu
  • 50,556
  • 8
  • 44
  • 80
1

I assume that because the button click doesn't await the method, SynchronousCode1() may execute before SynchronousCode2().

Yes, that is correct. It will depend on the implementation of someOtherAsyncMethod(). But assuming that method does not complete synchronously (as is its right), then it will eventually yield, causing the await someOtherAsyncMethod() to yield to the caller, which will allow the SynchronousCode1() method to be called, potentially before the someOtherAsyncMethod() method has completed (and definitely before then, as long as the asynchronous completion of someOtherAsyncMethod() is of any significant length of time).

Let's say the async method that is awaited takes a long time, like 2 seconds, and is an IO bound task. Will the SynchronousCode2() method finish on the same thread (or should I say same context) that that the button_push() method was called on?

Given the code you posted, and making the assumption that the code is executed in a typical UI thread (i.e. one with a thread-specific synchronization context), then yes, the SynchronousCode2() method will be executed in the same thread where the button_push() method was originally called.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
0

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();
}
tymtam
  • 31,798
  • 8
  • 86
  • 126