AJ Richardson
AJ Richardson

Reputation: 6830

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.
}

Upvotes: 9

Views: 1609

Answers (2)

AJ Richardson
AJ Richardson

Reputation: 6830

It appears that something strange is happening with OnHandleCreated. My solution was to use OnLoad instead. I'm pretty happy with this solution because there is really no reason to use OnHandleCreated in my situation.

I'm still curious as to why this is happening, so if anyone knows, feel free to post another answer.

Edit:

I found the real problem: it turns out that I was calling Form.ShowDialog() after a ConfigureAwait(false). As such, the form was being constructed on the UI thread, but then I was calling ShowDialog on a non-UI thread. I'm surprised that this worked at all.

I've removed the ConfigureAwait(false) so now ShowDialog is getting called on the UI thread.

Upvotes: 2

Stephen Cleary
Stephen Cleary

Reputation: 457472

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.

Upvotes: 8

Related Questions