我故意滥用 Windows 窗体应用程序中的消息循环,但我的“只是为了好玩”项目很快就超出了我的理解水平。当任务正在运行时,表单没有响应。是的,还有很多其他类似的问题,但就我而言,我故意避免在另一个线程上工作(为了赢得对自己的赌注?)
我有一个在 UI 线程上运行(许多)短时间片段的函数:
get_IsComplete()
检查任务是否完成; DoWork()
从0到1000循环(只是为了让CPU保持温暖)。该任务通过调用 control.BeginInvoke(new Action(ContinueWith), control);
启动,随后它(尾递归)调用自身直到完成,始终在 UI 线程上运行一小部分工作。
public void ContinueWith(Control control)
{
if (!IsComplete)
{
DoWork();
OnNext(control);
control.BeginInvoke(new Action(ContinueWith), control);
}
else
{
OnCompleted(control);
}
}
我希望应用程序能够处理其他事件(鼠标单击、控件重绘、表单移动等),但似乎我的调用比我想要的更优先。
有什么建议吗?
control.BeginInvoke()
调用将您传递的委托放入内部队列中,并调用PostMessage()
来唤醒消息循环并注意。 这就是第一个 BeginInvoke
发生的原因。 任何输入事件(鼠标和键盘)也会进入消息队列,Windows 将它们放在那里。
您没有预料到的行为是在检索发布的消息时运行的代码中。 它不只是将 one 调用请求出队并执行它,它会循环直到整个调用队列被清空。根据代码的工作方式,该队列永远不会清空,因为调用
ContinueWith()
会添加另一个调用请求。因此它只是不断循环和处理调用请求,而永远不会从消息队列中检索更多消息。或者换句话说:它正在泵送调用队列,而不是消息队列。
输入消息保留在消息队列中,直到代码停止添加更多调用请求并且在代码停止递归后恢复常规消息循环泵送。发生这种情况时,您的 UI 将看起来冻结,因为
Paint
事件也不会被传递。它们仅在消息队列为空时生成。
重要的是它按其方式工作,
PostMessage()
调用不能保证工作。 Windows 不允许消息队列中的消息超过 10,000 条。但Control.BeginInvoke()
没有这样的限制。通过完全清空调用队列,丢失的 PostMessage
消息不会导致任何问题。 但这种行为确实会导致其他问题。一个典型的例子是过于频繁地拨打BackgroundWorker.ReportProgress()
。 同样的行为,UI 线程只是被调用请求淹没,并且不再逃避其正常职责。 对遇到这种情况的任何人都皱起眉头:“我正在使用 BackgroundWorker,但我的 UI still 冻结了”。
总之,你的实验是彻底的失败。 需要调用
Application.DoEvents()
来强制清空消息队列。有很多注意事项,请查看此答案了解详细信息。即将推出的对 async 关键字的支持将提供另一种方法来做到这一点。 不太确定它是否以不同的方式对待消息优先级。我相当怀疑,Control.BeginInvoke()
是相当核心的。 解决这个问题的一种方法是使用带有非常短的 Timer
的 Interval
。计时器消息也进入消息队列(某种程度上),但它们的优先级非常低。首先处理输入事件。或者一个低级黑客:用您自己的消息调用 PostMessage
并覆盖 WndProc
来检测它。这有点偏离直线和狭窄。 Application.Idle
事件对于在检索任何输入事件后进行处理非常有用。
使用优先的 begininvoke 重载。 “正常”优先级高于输入和渲染。 你需要选择像applicationidle这样的东西