async-await的延续突发 – 表现不同?

我有一个winform代码,在单击按钮后运行:

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); } 

题 :

为什么我会在delay=1时一次看到一个MessageBox:

在此处输入图像描述

但如果我把延迟改为: 1,2,3

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

我看到 – 3个消息框甚至没有点击任何消息框?

在此处输入图像描述

  • 感谢@Noseratio 将该行为指向第一位 。

请注意,嵌套的消息循环是邪恶的,因为意外的重入是刚刚太难(tm)。

我认为解释这种行为有两个关键的理解。 第一个是异步延续 – 就像所有其他“运行此任意代码”Win32消息一样 – 具有比其他消息更高的优先级 。 第二个问题是, 在运行嵌套消息循环时, Win32有一个长期存在的传播方式,即发送消息并同步阻止响应。 (另一方面,我个人认为,这种可怕的重入 – Win32 API的所有设计都是Windows上绝大多数应用程序错误的原因)。

如果以保留堆栈跟踪的方式运行代码,则可以更清楚地看到正在发生的事情:

 void button1_Click(object sender, EventArgs e) { AAA(); } private List stacks = new List(); 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)); } 

在对话框全部关闭(首先是最小,然后是中等,然后是最大)之后, 将对话框文本 (最大堆栈,然后是中等,然后最小)与剪贴板进行比较。 很明显,对话框的显示顺序相反。

相信这样的事情正在发生,但没有信心肯定地说:

  • 第一个延迟触发并调用MessageBox.Show
  • Win32 MessageBox函数启动一个嵌套的消息循环,并开始设置实际的对话框,其中包含消息(即设置标题,文本等)。 请注意,这些调用会输出消息,但它们尚未准备好显示对话框。
  • 第二个延迟会自动调用MessageBox.Show ,然后在这些设置消息前面跳转并跳转。
  • 同样的第三次延迟。 第三个延迟的消息框实际上完成了它的设置并显示出来。 另外两个消息框仍然(同步)等待它们的消息循环返回一个值,但由于这些循环正在运行代码,它们无法返回。

当您将计时更改为1, 2, 3 ,您仍将在剪贴板中获得相同的堆栈,但您将看到对话文本现在按顺序排列(最小堆栈首先,然后是中等,然后是最大堆栈)。 这是因为每个MessageBox.Show都有足够的时间来设置消息框并建立其消息循环,并在其上面的下一个图层之前显示对话框。

理论上, MessageBox.ShowAsync API可以避免这种奇怪的行为,完全避免嵌套循环。 不过,我不会屏住呼吸。