【问题标题】:WinForms message loop not responsiveWinForms 消息循环无响应
【发布时间】:2011-07-22 11:09:07
【问题描述】:

我故意在 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);
    }
}

我希望应用程序处理其他事件(鼠标单击、控件重绘、表单移动等),但似乎我的调用获得了比我想要的更高的优先级。

有什么建议吗?

【问题讨论】:

  • Hans 是正确的,你没有这样的地方,执行永远不会离开函数。我想说的是,你可以通过使用计时器来避免它,但他也这么叫!不要像这样使用调用 / BeginInvoke。无论如何,在多线程情况之外这样做是毫无意义的;你可以直接调用代码。在你继续之前,请点击他的链接;这是一个非常简洁的解释。大多数 90 年代的区域 C windows API 程序员会在这上面写 20 段(我最不耐烦的用户谢谢你,Hans)。

标签: c# winforms multithreading


【解决方案1】:

control.BeginInvoke() 调用将您传递的委托放入内部队列并调用 PostMessage() 以唤醒消息循环并注意。这就是第一个 BeginInvoke 进行的原因。任何输入事件(鼠标和键盘)也会进入消息队列,Windows 将它们放在那里。

您没有预料到的行为是在检索发布的消息时运行的代码中。它不只是将 one 调用请求出列并执行它,它会循环直到整个调用队列被清空。您的代码的工作方式是,该队列永远不会被清空,因为调用 ContinueWith() 会添加另一个调用请求。所以它只是不断循环和处理调用请求,并且永远不会从消息队列中检索更多消息。或者换一种说法:它是在抽取调用队列,而不是消息队列。

在您的代码停止递归之后,输入消息将保留在消息队列中,直到您的代码停止添加更多调用请求并且常规消息循环泵送恢复。发生这种情况时,您的 UI 将看起来冻结,因为 Paint 事件也不会被传递。它们仅在消息队列为空时生成。

重要的是它的工作方式, PostMessage() 调用不能保证工作。 Windows 不允许消息队列中的消息超过 10,000 条。但是 Control.BeginInvoke() 没有这样的限制。通过完全清空调用队列,丢失的 PostMessage 消息不会导致任何问题。但是,此行为确实会导致其他问题。一个经典的方法是过于频繁地调用 BackgroundWorker.ReportProgress()。同样的行为,UI 线程只是充斥着调用请求,不再绕过它的正常职责。对遇到这种情况的任何人皱眉头:“我正在使用 BackgroundWorker,但我的 UI 仍然冻结”。

Anyhoo,你的实验非常失败。需要调用 Application.DoEvents() 来强制清空消息队列。对此有很多警告,请查看this answer 了解详情。即将推出的对 async 关键字的支持将提供另一种方法来做到这一点。不确定它是否以不同的方式处理消息优先级。我相当怀疑,Control.BeginInvoke() 是相当核心的。解决该问题的一种方法是使用间隔非常短的计时器。计时器消息也进入消息队列(有点),但它们的优先级很低。首先处理输入事件。或者一个低级的黑客攻击:用你自己的消息调用 PostMessage 并覆盖 WndProc 来检测它。这有点偏离直线和狭窄。 Application.Idle 事件对于在检索到任何输入事件后进行处理很有用。

【讨论】:

  • 汉斯,感谢您抽出宝贵时间给出如此详细的答案。您正确地指出,我的误解是消息处理循环在开始绘画之前清空队列的行为。有了这样的答案,它远不是一个令人沮丧的失败,我已经创建了一个玩具 WPF 应用程序(根据 Robert Levy 的建议,如下)以避免调用 Application.DoEvents()。 WPF 模型是否可以被认为是一种避免线程的好方法,还是会像你这样的 stackoverflow 高额玩家总是回避它?
  • 我个人尽可能避免线程,太多伤痕。我尽可能使用状态机,async 关键字在后台执行的操作。与我所做的编程类型(机器控制)有关。而且不是 WPF 粉丝,不知道 Dispatcher.BeginInvoke() 的确切细节足以跟进。你可以提出另一个关于它的问题。
【解决方案2】:

使用优先级的 begininvoke 重载。 “正常”优先级高于输入和渲染。你需要选择类似applicationidle的东西

【讨论】:

  • Err... 我在想 WPF 和 dispatcher.begininvoke。使用 winforms 认为 emiswelt 是正确的,您会遇到丑陋的 DoEvents 调用
  • 我没有看到任何将优先级作为参数的 System.Windows.Forms.Control.BeginInvoke 的公共重载。我在哪里可以找到它们?
  • 啊,好的,谢谢。如果 WPF 提供这种方法,那么我可以尝试我的 v0.2 实验的 WPF 版本!
  • WPF 允许我这样做的方式给我留下了深刻的印象;恕我直言,它比 WinForms 方式更好。我使用的代码是: control.Dispatcher.BeginInvoke(new Action(ContinueWith), DispatcherPriority.Background, control);
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-03
  • 2012-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-25
相关资源
最近更新 更多