Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

await without ConfigureAwait(false) continues on a different thread

I have a WinForms app, and I have some code that needs to run on the UI thread. However, the code after the await runs on a different thread.

protected override async void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);

    // This runs on the UI thread.
    mainContainer.Controls.Clear();

    var result = await DoSomethingAsync();

    // This also needs to run on the UI thread, but it does not.
    // Instead it throws an exception:
    // "Cross-thread operation not valid: Control 'mainContainer' accessed from a thread other than the thread it was created on"
    mainContainer.Controls.Add(new Control());
}

I also tried explicitly adding ConfigureAwait(true), but it makes no difference. My understanding was that if I omit ConfigureAwait(false), then the continuation should run on the original thread. Is this incorrect in some situations?

I've also noticed that if I add a control to the collection before the await, then the continuation magically runs on the correct thread.

protected override async void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);

    // This runs on the UI thread.
    mainContainer.Controls.Add(new Control());
    mainContainer.Controls.Clear();

    var result = await DoSomethingAsync();

    // This also runs on the UI thread now. Why?
    mainContainer.Controls.Add(new Control());
}

My question is:

  1. Why is this happening?
  2. How do I convince the continuation to run on the UI thread (ideally without doing my hack of adding a control and removing it)?

For reference, here are the important parts of DoSomethingAsync. It submits an HTTP request using RestSharp.

protected async Task DoSomethingAsync()
{
    IRestRequest request = CreateRestRequest();

    // Here I await the response from RestSharp.
    // Client is an IRestClient instance.
    // I have tried removing the ConfigureAwait(false) part, but it makes no difference.
    var response = await Client.ExecuteTaskAsync(request).ConfigureAwait(false);

    if (response.ResponseStatus == ResponseStatus.Error)
        throw new Exception(response.ErrorMessage ?? "The request did not complete successfully.");

    if (response.StatusCode >= HttpStatusCode.BadRequest)
        throw new Exception("Server responded with an error: " + response.StatusCode);

    // I also do some processing of the response here; omitted for brevity.
    // There are no more awaits.
}
like image 710
AJ Richardson Avatar asked Aug 25 '15 18:08

AJ Richardson


1 Answers

My understanding was that if I omit ConfigureAwait(false), then the continuation should run on the original thread. Is this incorrect in some situations?

What actually happens is that await will capture the current context by default, and use this context to resume the async method. This context is SynchronizationContext.Current, unless it is null, in which case it is TaskScheduler.Current (usually the thread pool context). Most of the time, the UI thread has a UI SynchronizationContext - in the case of WinForms, an instance of WinFormsSynchronizationContext.

I've also noticed that if I add a control to the collection before the await, then the continuation magically runs on the correct thread.

No thread starts with a SynchronizationContext automatically. The WinForms SynchronizationContext is installed on-demand when the first control is created. This is why you're seeing it resume on a UI thread after creating a control.

Since moving to OnLoad is a workable solution, I recommend you just go with that. The only other option (to resume on the UI thread before a control is created) is to manually create a control before your first await.

like image 67
Stephen Cleary Avatar answered Nov 04 '22 02:11

Stephen Cleary