Lake_Lagunita
Lake_Lagunita

Reputation: 553

Why these two C# code blocks about Task behaves differently?

I am studying C# Task, async/await and I have encountered a situation that I do not quite understand. I made two code snippet, the first one is based upon console application, and the second one is based upon wpf. Here are the two code snippet:

  1. code for console application:
async static Task Main()
{
    Helper.PrintThreadId("Before");
    await FooAsync();
    Helper.PrintThreadId("After");
}

async static Task FooAsync()
{
    Helper.PrintThreadId("Before");
    await Task.Delay(1000);
    Helper.PrintThreadId("After");
}

class Helper
{
    private static int index = 1;
    public static void PrintThreadId(string message = null, [CallerMemberName] string name = null)
    {
        var title = $"{index}: {name}";
        if (!string.IsNullOrEmpty(message))
            title += $" @ {message}";
        Console.WriteLine("Thread ID: " + Environment.CurrentManagedThreadId + ", title: " + title);
        Interlocked.Increment(ref index);
    }
}
  1. code for wpf (Helper is exactly same as in console application):
async Task<int> HeavyJob()
{
    Helper.PrintThreadId("Before");
    await Task.Delay(3000);
    Helper.PrintThreadId("After");
    return 10;
}

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Helper.PrintThreadId("Before");
    var res = await HeavyJob().ConfigureAwait(false);
    Helper.PrintThreadId("After");
}

class Helper
{
    private static int index = 1;
    public static void PrintThreadId(string message = null, [CallerMemberName] string name = null)
    {
        var title = $"{index}: {name}";
        if (!string.IsNullOrEmpty(message))
            title += $" @ {message}";
        Debug.WriteLine("Thread ID: " + Environment.CurrentManagedThreadId + ", title: " + title);
        Interlocked.Increment(ref index);
    }
}

Then, for console application, the result is:

![enter image description here

for wpf, the result is:

![enter image description here

My question is the third line of the two results. why these two behaves differently? As I understand, the thread ID in the third line and fourth line should be the same.

I hope if anyone could explain this question. Thank you.

Upvotes: 0

Views: 87

Answers (2)

IS4
IS4

Reputation: 13217

When you await a task, there is actually no guarantee the code that follows gets scheduled to a different thread. Rather, SynchronizationContext.Current is used to determine where the continuation should be executed.

For WPF/WinForms applications, there is a main loop that handles all events, including scheduled tasks. In such a context, awaiting a task is able to resume the code on the original thread. This is important because the controls are not thread-safe; they should be accessed only from the main thread. This is the reason you see HeavyJob @ After resumed on the same thread.

In console applications, there is no main loop and no need to keep the bulk of the code single-threaded. Hence continuations are executed on various other threads (for example from the thread pool), since there is no way to return back to the original thread.

Now with this explanation, you would expect all code after await to run on the same thread on WPF, but this is actually not the case here. The reason is that you used .ConfigureAwait(false) ‒ this specifically instructs the awaiter not to use the original synchronization context, reverting back to the default behaviour you see in case of the console application. I'd argue this is actually not a good place to use ConfigureAwait(false) ‒ I'd stick to the normal behaviour for GUI event handlers, since you are expected to work with GUI controls, and use .ConfigureAwait(false) in the inner logic where resuming on the same thread is no longer necessary (once you don't have any more controls to work with).

Upvotes: 1

Matthew Watson
Matthew Watson

Reputation: 109792

In the WPF code in HeavyJob() you have this:

await Task.Delay(3000);

That does not have .ConfigureAwait(false) so for a WPF (or WinForms) application it will resume on the same thread that it was started on - that will be thread 1 in your example. It can do this because the SynchronizationContext has the ability to use the Windows message loop to resume on the UI thread.

Similarly in the console code in FooAsync() you also have an await without .ConfigureAwait(false). However, for console apps by default there is no SynchronizationContext available to use to resume on the calling thread. So in this case the await resumes on a new thread.

Upvotes: 2

Related Questions