Pregunta

A simple question. Here is a part of a WinForms app:

void Form1_Load(object sender, EventArgs e)
{
    var task2 = Task.Factory.StartNew(() => MessageBox.Show("Task!"),
        CancellationToken.None,
        TaskCreationOptions.None,
        TaskScheduler.FromCurrentSynchronizationContext());

    Debug.WriteLine("Before Exit");
    MessageBox.Show("Exit!");
}

Could someone please explain why I see "Task!" first, and then "Exit" when I run this?

And when I see the "Task!" message box, "Before Exit" has been already printed in the debug output.

EDIT: More simple, same effect:

void Form1_Load(object sender, EventArgs e)
{
    SynchronizationContext.Current.Post((_) => 
        MessageBox.Show("Task!"), null);

    Debug.WriteLine("Before Exit");
    MessageBox.Show("Exit!");
}

EDIT: If I replace MessageBox.Show("Exit!") with Form { Text = "Exit!" }.ShowDialog(), I see "Exit", then "Task", as expected. Why is that?

¿Fue útil?

Solución

The exact details of Win32 message pumping in the .NET framework are undocumented. Note that the Win32 API has historically permitted reentrant behavior.

When your code starts the task, the task scheduler it's using will request the task to execute on the current SynchronizationContext. So it ends up calling SynchronizationContext.Current.Post, very similar to your second code example.

The WindowsFormsSynchronizationContext works by posting a Win32 message to the message queue; the message type is one defined internally by .NET to mean "execute this code". (These details are undocumented).

When your code proceeds to call MessageBox.Show, it runs a nested message loop, and that is why the queued actions are executing. If you removed the Exit! message box, then the actions would execute after Form1_Load returns to the top-level message loop in Application.Run.

Otros consejos

I'm guessing what happens is this:

  • You start the task with the current synchronization context. This results in a message being posted (asynchronously) to the current thread's message queue, saying "when you process this message, run this code".
  • You do a MessageBox.Show. This creates a new window, which results in the sending of messages (synchronously) to the new window as part of its creation cycle.
  • A side effect of SendMessage is that it dispatches messages. So the posted message is now processed synchronously, along with anything else on the message queue.

Therefore, the act of creating the new message-box window is probably what gives the task a chance to execute first. If you instead did a Debug.WriteLine and a Thread.Sleep, you would probably see the opposite: the Sleep would happen first (since you're not pumping messages) and then the Task would run.

Edit: Based on the comments, the OP's behavior occurs when calling ShowMessage, but not when doing new Form. Those both involve sending window-creation messages, so obviously SendMessage does not synchronously process all the messages that were already on the queue. (The post I linked explains that SendMessages does dispatch sent messages, but doesn't specifically say that it dispatches messages already on the queue -- I just assumed the latter, obviously incorrectly.)

I think I was on the right track -- something about the MessageBox.Show call is causing the message to get handled before the dialog is displayed, in a way different from what happens with new Form().ShowDialog() -- but I wasn't correct about the specific mechanic involved. Anyone else, feel free to research this and come up with a more accurate answer.

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 :))

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top