Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

async-await's continuations bursts — behave differently?

Tags:

I have a winform code which run after a button click :

void button1_Click(object sender, EventArgs e) {     AAA(); }   async Task BBB(  int delay) {     await Task.Delay(TimeSpan.FromSeconds(delay));     MessageBox.Show("hello");   }  async Task AAA() {     var task1 = BBB(1);  // <--- notice delay=1;       var task2 = BBB(1);  // <--- notice delay=1;       var task3 = BBB(1);  // <--- notice delay=1;       await Task.WhenAll(task1, task2, task3); } 

Question :

Why do I see one MessageBox at a time when delay=1 :

enter image description here

But If I change delay to : 1,2,3

    var task1 = BBB(1);       var task2 = BBB(2);       var task3 = BBB(3);   

I see - 3 Message Boxes without even clicking any Messagebox?

enter image description here

  • Thanks to @Noseratio for pointing that behaviour at first place.
like image 841
Royi Namir Avatar asked Sep 14 '15 20:09

Royi Namir


1 Answers

Please note that nested message loops are evil because unexpected reentrancy is Just Too Darn Hard(tm).

I think there are two key pieces of understanding to explain this behavior. The first is that async continuations - like all other "run this arbitrary code" Win32 messages - have a higher priority than other messages. The second is that there's a long-standing Win32 tradition of sending messages and synchronously blocking for a response while running a nested message loop. (On a side note, it is my personal opinion that this horrible reentrancy-everywhere design of the Win32 API has been responsible for the vast majority of application bugs on Windows).

If you run your code in a way that preserves stack traces, you can see more clearly what's going on:

void button1_Click(object sender, EventArgs e) {     AAA(); }  private List<string> stacks = new List<string>();  async Task BBB(int delay) {     await Task.Delay(TimeSpan.FromSeconds(delay));     var stack = new StackTrace().ToString();     stacks.Add(stack);     MessageBox.Show(stack); }  async Task AAA() {     var task1 = BBB(1);  // <--- notice delay=1;       var task2 = BBB(1);  // <--- notice delay=1;       var task3 = BBB(1);  // <--- notice delay=1;       await Task.WhenAll(task1, task2, task3);     Clipboard.SetText(string.Join("\r\n\r\n", stacks)); } 

Compare the dialog texts (largest stack first, then medium, then smallest) with the clipboard after the dialogs are all closed (smallest first, then medium, then largest). It's clear that the dialogs are being displayed in the reverse order.

I believe something like this is happening, but lack the confidence to say for sure:

  • The first delay fires off and calls MessageBox.Show.
  • The Win32 MessageBox function starts a nested message loop and starts setting up the actual dialog with messages to itself (i.e., setting caption, text, etc). Note that these calls pump messages but they're not ready to show the dialog yet.
  • The second delay fires off and jumps in front of those setup messages with its own call to MessageBox.Show.
  • Similarly for the third delay. The third delay's message box actually completes its setup and gets shown. The other two message boxes are still (synchronously) waiting for their message loops to return a value, but because those loops are running code, they can't return.

When you change the timings to 1, 2, 3, you'll still get the same stacks in the clipboard, but you'll see the dialog texts are now in order (smallest stack first, then medium, then largest). This is because each MessageBox.Show has sufficient time to set up the message box and establish its message loop and show the dialog before the next one layers on top of it.

In theory, this odd behavior could be avoided by a MessageBox.ShowAsync API that avoids the nested loops completely. I wouldn't hold my breath for it, though.

like image 199
Stephen Cleary Avatar answered Oct 20 '22 01:10

Stephen Cleary