9

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.
}
AJ Richardson
  • 6,610
  • 1
  • 49
  • 59
  • 1
    Why do you think this runs on a different thread? Where does this code runs? event handler? – i3arnon Aug 25 '15 at 18:40
  • I know it runs on a different thread because the code after the await throws an exception: "Cross-thread operation not valid: Control 'mainContainer' accessed from a thread other than the thread it was created on". – AJ Richardson Aug 25 '15 at 18:42
  • Yes, the code runs in an event handler. I will update my question. – AJ Richardson Aug 25 '15 at 18:42
  • Which event does `OnHandleCreated` is registered to? Because event handlers in winforms usually have 2 parameters. – i3arnon Aug 25 '15 at 18:47
  • [Control.HandleCreated](https://msdn.microsoft.com/en-us/library/system.windows.forms.control.handlecreated(v=vs.110).aspx) – AJ Richardson Aug 25 '15 at 18:48
  • Then the signatures don't match – i3arnon Aug 25 '15 at 18:49
  • I'm overriding `OnHandleCreated` because I'm inheriting form [`Form`](https://msdn.microsoft.com/en-us/library/system.windows.forms.form(v=vs.110).aspx). – AJ Richardson Aug 25 '15 at 18:49
  • Well, you either found a bug in .Net or you're missing something. What's `DoSomethingAsync`? – i3arnon Aug 25 '15 at 18:57
  • @i3arnon: see my updated question. – AJ Richardson Aug 25 '15 at 19:09
  • @AJRichardson, can you consistently reproduce the issue? Does omitting the call to `Controls.Add()` *always* result in the post-await part running on the wrong thread, and does adding the former *always* result in the latter running on the right thread? – Frédéric Hamidi Aug 25 '15 at 19:12
  • @FrédéricHamidi: Yes – AJ Richardson Aug 25 '15 at 19:16
  • Dark magic is definitely at play, then. Sure, you're asking for trouble by overriding a method linked to a native event handler and making it async at the same time. I don't know how the runtime will handle this situation. Then again, the behavior of `Controls.Add()` is quite peculiar indeed. – Frédéric Hamidi Aug 25 '15 at 19:20
  • @AJRichardson Can you check what's in `SynchronizationContext.Current` before and after the `await`? – i3arnon Aug 25 '15 at 19:27
  • What if you minimize your async void OnHandleCreated method and move all the code into an async Task OnHandleCreatedAsync, as suggested in [Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx)? – Joel V. Earnest-DeYoung Aug 25 '15 at 19:29
  • @i3arnon How do I check the contents of `SynchronizationContext.Current`? It's just an object with some methods but no properties. – AJ Richardson Aug 25 '15 at 19:52
  • @AJRichardson what matters is whether that is null or not. Is it null before or after the await? – i3arnon Aug 25 '15 at 19:54
  • @i3arnon No, it is never null. – AJ Richardson Aug 25 '15 at 20:03
  • @AJRichardson that means that in both cases you are running on the UI thread. Are you sure you're getting that exception there? – i3arnon Aug 25 '15 at 20:05
  • I checked `Thread.Current` and it is different before and after the await (in the first case only). – AJ Richardson Aug 25 '15 at 20:06
  • 1
    There is definitely dark magic involved with the `HandleCreated` event. I switched to using the `Load` event, and everything works fine. I'm not sure why `HandleCreated` was being used, anyways. (I inherited this code from others, and a lot of it is very messy). – AJ Richardson Aug 25 '15 at 20:06

2 Answers2

8

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Actually, `WindowsFormsSynchronizationContext` gets installed in `Control`'s constructor [here](http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,535), so it should be there already when `OnHandleCreated` is called. This has to be something else. There's a possibly related [bug](https://connect.microsoft.com/VisualStudio/feedback/details/806432/application-doevents-resets-the-threads-current-synchronization-context), but in that case both `OnHandleCreated` and `OnLoad` would be affected. – noseratio Aug 26 '15 at 05:56
2

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.

AJ Richardson
  • 6,610
  • 1
  • 49
  • 59
  • Could you place `Debug.WriteLine(new { SynchronizationContext.Current })` right before `await DoSomethingAsync()` inside `OnHandleCreated`? Does it show `System.Windows.Forms.WindowsFormsSynchronizationContext` (expected) or `System.Threading.SynchronizationContext` ? – noseratio Aug 26 '15 at 05:32
  • 1
    I get a `System.Threading.SynchronizationContext` before the await, and null after the await. I'm not sure why I was getting a non-null value yesterday. But I guess that explains it. I also noticed that if I run the same code in the same project on a different branch in our repo, I get a `System.Windows.Forms.WindowsFormsSynchronizationContext`, and everything works properly. I wonder if it has something to do with the way the form is getting initialized. – AJ Richardson Aug 26 '15 at 15:14
  • With your recent edit, you should mark your answer as accepted as it explains what actually happened. Otherwise it's safe to use `await` in `OnHandleCreated`, see my [comment](http://stackoverflow.com/questions/32211598/await-without-configureawaitfalse-continues-on-a-different-thread/32213206?noredirect=1#comment52321127_32213644) to Stephen's answer. – noseratio Aug 26 '15 at 21:55
  • 1
    @Noseratio Yeah, I have to wait 2 days before I can accept my own answer. I will accept it once SO lets me :) – AJ Richardson Aug 27 '15 at 13:28