Okay, let's lay down the facts.
MessageBox.Show
creates its own message pump. This uses the current ThreadContext
, which I assume will be the same as the UI thread in your case - in other words, your application is frozen. Show
is modal, despite what the name might suggest.
MessageBox
is not a Form
- it's created by user32.dll
and that's also where its message pump is.
- Your method of creating the task will ultimately result in pushing the task to
ThreadPool.QueueUserWorkItem
. The queue seems to be maintained per-thread (it's thread-static). Things get complicated when you ask when the task gets actually executed, because now we're dealing with asynchronous callbacks from the outside of .NET. EDIT: I was wrong. The fact is that the current synchronization context is a derived class, WindowsFormsSynchronizationContext
, which actually does put the work item in the invoke queue, which is tied to windows messaging.
- Everything happens on one thread, as simple as that.
- The task is executed after the debug output. It has nothing to do with the
MessageBox.Show
inside the task.
If I add another await
after the debug output (await Task.Delay(1000);
), a fun thing happens - "Task!" is shown, followed by "Exit!" a second later. Two message boxes at once?! What witchcraft is responsible for this?!
It's obvious that the "Exit!" is a modal of the "Task!" form, not our parent. In other words, the second message box somehow got to run "in the context" of the first message box.
This is related to what I was talking about in my original answer. The modal box steals the thread it's running on, and handles the message pumping. When the second await
executes, it runs on our "Task!" form, not the (blocked) parent form.
If we use Thread.Sleep(1000);
instead of the await
, this behaviour is lost. However, the Thread.Sleep
does run before the "Task!" message box, as evidenced by the fact that as soon as we close the "Task!" form, "Exit!" appears immediately, rather than waiting for a second, while "Task!" has the delay.
The forms rely on windows messaging. Modal forms "steal" the handle of their owner, and process those messages. It is only after the message box closes that a WM is sent to the parent (a simple "set focus" message).
However, await
in our scenario works on the UI thread, but outside of the message loop. So when we awaited after the first dialog was shown, the code after the await executed as if it was run inside the first dialog - the owner of the MessageBox is determined before creating the underlying native message box (it's not a .NET Form!), so it gets the current active window - in our awaited case, that's the "Task!" form. Mystery solved.
The mystery that remains is why the task is run somewhere between the MessageBox.Show("Exit!");
call and the message box actually stealing the message loop.
This brings us to the grand finale:
We create our little task. However, it has a windows forms synchronization context, so instead of doing anything, it simply adds the task to the queue on the form. This is on top of the queue, so it will be executed as soon as we relieve control of the UI thread.
If we await before showing the "Exit!" dialog, all is clear - the "Task!" is shown first, and at some point (because it's not passed through the messaging queue), the "Exit!" shows as its child.
If we don't await, the MessageBox.Show("Exit!");
will enter a modal message loop (we can tell thanks to the Application.EnterThreadModal
event). Then, the WinAPI (user32.dll) MessageBox
method is called, which immediately pumps. This reads the queued WM related to our queued Invoke
call - the "Task!"'s task. It gets invoked immediately, and effectively blocks the original Message.Show call, because that one can't process its own messages.
All in all, another good reason not to complicate things on the UI thread. As it seems, be especially vary of MessageBox.Show
, because it does a lot more than meets the eye.
In reality, you would run your task outside of the UI thread, and only the continuation that needed access to UI would be in the UI thread. Still, it's very interesting how MessageBox
hijacks what happens - this could backfire spectacularly if your background task gets stuck invoking something on the UI thread, which is effectively taken over by the message box; there goes your asynchronicity :))