【问题标题】:async-await's continuations bursts — behave differently?async-await 的延续爆发——表现不同?
【发布时间】:2015-12-10 23:28:55
【问题描述】:

我有一个在单击按钮后运行的 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 个消息框,甚至没有点击任何消息框?

【问题讨论】:

  • 我的猜测是模态对话框使用了某种“嵌套消息泵”。当您一次全部推送它们时,它可能会在“外部消息泵”上批量处理整组排队的调用,从而导致一系列调用的阻塞行为。展开后,后续调用可能会在连续嵌套的消息泵中安排/处理,从而产生如您所见的一连串调用。
  • 如果您不捕获 WindowsFormsSynchronizationContext await Task.Delay(TimeSpan.FromSeconds(delay)).ConfigureAwait(false); 则将显示所有消息框(延迟等于 1 时的事件)。
  • 这是一个有趣的问题。您是如何发现这个问题的?
  • @Brian 来自 cmets in here

标签: c# winforms async-await


【解决方案1】:

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

我认为有两个关键的理解部分可以解释这种行为。首先是异步延续 - 与所有其他“运行此任意代码”Win32 消息一样 - 比其他消息具有更高的优先级。第二个是 Win32 发送消息和同步在运行嵌套消息循环时阻塞响应的长期传统。 (附带说明一下,我个人认为,Win32 API 的这种可怕的可重入性无处不在的设计是造成 Windows 上绝大多数应用程序错误的原因)。

如果您以保留堆栈跟踪的方式运行代码,您可以更清楚地看到发生了什么:

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(首先是最大的堆栈,然后是中的堆栈,然后是最小的)在对话框全部关闭后(首先是最小的,然后是中的,然后是最大的)剪贴板。很明显,对话框是以相反的顺序显示的。

相信这样的事情正在发生,但缺乏信心说肯定

  • 第一个延迟触发并调用MessageBox.Show
  • Win32 MessageBox 函数启动一个嵌套消息循环,并开始设置带有消息的实际对话框给它自己(即设置标题、文本等)。请注意,这些调用会泵送消息,但它们还没有准备好显示对话框。
  • 第二个延迟触发并跳到这些设置消息的前面,并调用了自己的 MessageBox.Show
  • 第三次延迟也是如此。第三个延迟的消息框实际上完成了它的设置并显示出来。其他两个消息框仍在(同步地)等待它们的消息循环返回一个值,但由于这些循环正在运行代码,它们无法返回。

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

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

【讨论】:

  • 有趣的是,使用Form.ShowDialog 而不是MessageBox.Show 没有像这样的重入“竞赛”。它们将全部显示在彼此之上,尽管以不可预测的Task.Delay 延续顺序显示。我猜这是因为 WinFroms 实现了自己的模态消息循环逻辑,而不依赖于 Win32 DialogBox API。
  • @Noseratio:很好的收获;我没想过要那样做!
猜你喜欢
  • 2013-05-25
  • 1970-01-01
  • 1970-01-01
  • 2020-05-24
  • 2019-05-24
  • 2020-09-25
  • 1970-01-01
  • 1970-01-01
  • 2020-03-25
相关资源
最近更新 更多