MFC/WIN32/C++:如何在执行计算密集型操作时分派消息?
由于此应用程序的具体情况,我更愿意看看是否有一种合理的方法可以在不使用第二个线程的情况下处理此问题(最初是一个带有大量全局静态变量的 DOS 应用程序,已转换为 Windows/MFC,但不是从头开始设计的)最多是多线程的 - 只是使其能够感知多个文档是一项重大任务)。
相关应用程序正在尝试执行计算量非常大的操作,这会产生修改当前文档的副作用。它希望能够更新主窗口和主窗口。执行该过程时迭代地显示文档窗口。
执行简单的方法:循环遍历工作项,在文档窗口上发出绘图,修改文档的基础数据,并使用指示 x 或 y 完成的状态文本更新状态栏,通常是有效的。但有时应用程序会停止更新状态栏和文档窗口(视图),直到整个作业完成,然后所有内容都会立即更新。
所以代码中没有任何挂起条件。即它永远不会失败。这纯粹是在持续时间内未能处理窗口消息的问题(因为这在很大程度上是单线程应用程序)。
我认为显而易见的方法是在任务循环中放置一个 PeekMessage() 循环来调度任何累积的消息。我假设这就是视觉更新停止发生的原因:视图、主机或线程的消息队列中必须有一条 Windows 消息阻止对屏幕的直接更新。
然而,也许问题是别的?
无论如何,对于我们的应用程序来说,忽略消息队列可能会花费很长的处理时间,这似乎是一个坏主意(如果用户只是因为缺乏处理我们的信息而询问任务管理器,那么他们会认为应用程序“停止响应”)消息队列)。
但是,以下循环将变得无限:
// dispatch the messages until we're out of them again...
for (MSG msg; PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); )
{
TraceMessage(msg.message, msg.wParam, msg.lParam);
if (msg.message == WM_QUIT)
return;
}
注意:TraceMessage() 只是将一条人性化的跟踪输出到调试器窗口,其中显示了消息和消息的内容。这是争论。在本例中,它是 WM_PAINT。
因此,即使请求删除绘制消息,它似乎也会永远留在队列中(或者以某种方式无限地生成新消息)。
问题:
我是否全部都错了,以及我们的应用程序无法更新状态栏和状态栏的原因。 view 是别的东西吗?
对于长时间的 cpu 或磁盘 I/o 密集型操作,是否有比在任务循环中放置 PeekMessage 循环更好的方法(这不涉及重新构建整个应用程序以使其更加多线程友好)?
我要采用的解决方案...
void DoSomethingLengthy()
{
CWaitCursor wait;
// disable our application windows much as if we were running a modal dialog
// in order to lock out the user from interacting with us until we're doing doing this thing
AfxGetMainWnd()->BeginModalState();
while (bMoreWorkToDo)
{
// empty any generated / received thread & window messages
for (MSG msg; PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE|PM_NOYIELD); )
AfxPumpMessage();
// for some reason, our cursor reverts to the normal pointer
// so force ourselves to continue to have the wait-cursor until we're really done
AfxGetMainWnd()->RestoreWaitCursor();
// here's where we do one work-item
// ...
}
// restore our prior state
AfxGetMainWnd()->EndModalState();
}
是的,这是非常古老的技术,而且不一定是最好的方法。然而,对于这种情况来说,它是可行的并且非常有用。我们已经有一个复杂的应用程序,它使用 OnIdle 机制来实现多种目的,这使得它作为一种可能的方法不太有吸引力。
I would prefer to see if there is a reasonable way to handle this without using a second thread due to the specifics of this application (originally a DOS app with lots of global static variables, converted to Windows/MFC, but not designed from the ground up to be multi-threaded - just making it multiple document aware was a major undertaking).
The application in question is trying to do a very computationally intensive operation which has the side-effect of modifying the current document. It wants to be able to update the main window & document windows iteratively as the process is performed.
Doing the simple approach: loop through the work-items, issuing drawing on the document's window, modifying the document's underlying data, and updating the status bar with status text indicating x of y complete usually works. But sometimes the app stops updating both the status bar and the document's window (view) until the entire job is completed, and then everything updates at once.
So there aren't any hang-conditions in the code. i.e. it never fails to complete. It's purely a matter of failing to process window messages for the duration (since this is a single threaded application, for the most part).
I thought the obvious approach would be to put a PeekMessage() loop within the task-loop to dispatch any accumulated messages. I was assuming that this is the reason that the visual updates stop occurring: there must be a windows message in either the view's, the main-frame's, or the thread's message queue blocking the direct updates to the screen.
However, perhaps the issue is something else?
Regardless, it just seems like a bad idea for our app to ignore the message queue for what can be eons of processing time (the user will perceive the app as "stopped responding" if they ask Task Manager just due to the lack of processing our message queue).
However, the following loop becomes infinite:
// dispatch the messages until we're out of them again...
for (MSG msg; PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); )
{
TraceMessage(msg.message, msg.wParam, msg.lParam);
if (msg.message == WM_QUIT)
return;
}
NOTE: TraceMessage() simply outputs a human-friendly trace to the debugger window of what message & arguments this is. In this case, it is WM_PAINT.
So the paint message, even though it's being requested to be removed, seems to sit in the queue forever (or new ones are being generated somehow infinitely).
Questions:
Could I have it all wrong, and the reason our applications stops being able to update the status bar & view is something else?
Is there a better approach to a long cpu or disk I/o intensive operation than placing a PeekMessage loop within the task loop (that doesn't involve re-architecting the entire application to be more multi-thread-friendly)?
Solution I'm going with...
void DoSomethingLengthy()
{
CWaitCursor wait;
// disable our application windows much as if we were running a modal dialog
// in order to lock out the user from interacting with us until we're doing doing this thing
AfxGetMainWnd()->BeginModalState();
while (bMoreWorkToDo)
{
// empty any generated / received thread & window messages
for (MSG msg; PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE|PM_NOYIELD); )
AfxPumpMessage();
// for some reason, our cursor reverts to the normal pointer
// so force ourselves to continue to have the wait-cursor until we're really done
AfxGetMainWnd()->RestoreWaitCursor();
// here's where we do one work-item
// ...
}
// restore our prior state
AfxGetMainWnd()->EndModalState();
}
Yes, this is very old technology, and not necessarily the best approach. It is however feasible and highly useful for this context. We already have a complex app that uses the OnIdle mechanic for a multitude of purposes, making it less attractive as a possible approach.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
每当队列为空且存在非空无效区域时,就会合成
WM_PAINT
。要停止WM_PAINT
消息,您必须将窗口区域标记为有效。这发生在正常绘画处理过程中的BeginPaint
/EndPaint
期间。当文档说“从 GetMessage 返回内部 WM_PAINT 消息后或PeekMessage 或通过 UpdateWindow 发送到窗口,系统不会发布或发送进一步的 WM_PAINT 消息,直到窗口失效或使用 RDW_INTERNALPAINT 标志再次调用 RedrawWindow set.”,假设消息已完全处理。如果您未能遵守
WM_PAINT
处理协议(调用BeginPaint
和EndPaint
等),则“不再有 WM_PAINT 消息”行为也不能被依赖。WM_PAINT
is synthesized whenever the queue is empty and there is a non-empty invalid region. To stopWM_PAINT
messages, you have to mark the window area valid. That happens duringBeginPaint
/EndPaint
in the normal course of paint processing.When the documentation says "After an internal WM_PAINT message is returned from GetMessage or PeekMessage or is sent to a window by UpdateWindow, the system does not post or send further WM_PAINT messages until the window is invalidated or until RedrawWindow is called again with the RDW_INTERNALPAINT flag set.", that assumes that the message was completely processed. If you fail to abide by the contract of
WM_PAINT
processing (callBeginPaint
andEndPaint
, among other things) then the "no more WM_PAINT messages" behavior can't be relied on either.PeekMessage
仅向前查看下一条消息。您希望GetMessage
将其从队列中删除,然后DispatchMessage
实际调用WndProc
。此时,您刚刚从 VB 中重新发明了
DoEvents
,这是 1996 年技术的主要内容。PeekMessage
only looks ahead at the next message. You wantGetMessage
to remove it from the queue, and thenDispatchMessage
to actually invokeWndProc
.At this point you just reinvented
DoEvents
from VB, the staple of 1996 technology.您的应用程序不会更新的原因是没有任何东西正在处理 WM_PAINT 消息。 WM_PAINT 的独特之处在于,每当 Windows 确定窗口的某些部分已过时时,它就会按需生成。这就是为什么你会得到无数个。
最好的解决方案是将您的长任务分成几部分,并使用 PostMessage 将自定义消息放入队列中,您可以使用该消息从中断处继续。
The reason your app won't update is because nothing is processing the WM_PAINT messages. WM_PAINT is unique in that it is generated on demand, whenever Windows determines that parts of a window are out of date. That's why you get an infinite number of them.
The best solution is to break your long task into pieces, and use PostMessage to place a custom message into the queue that you can use to continue where you left off.
其他人已经指出,WM_PAINT 通常是在需要时合成的,而不是消息队列中实际存在 WM_PAINT 消息。
谈到你真正的问题,在我看来,实际上只有两种真正的可能性:要么重新架构它,要么不管它。如果您打算不理会它,最好将其隔离,这样您的应用程序就可以在两个几乎完全独立的部分中运行:一个是几乎未更改的 DOS 应用程序,它的工作方式与往常一样。另一个是几乎完全独立的 GUI 前端。
具体如何执行取决于 DOS 应用程序如何生成其输出。如果它使用标准流(例如,使用 printf 等写入标准输出),则将其转换为 Win32 控制台应用程序可能是最简单的。让您的 GUI 应用程序生成控制台应用程序,并将其标准输入、标准输出和标准错误流重定向到父级的匿名管道中。然后,父 GUI 应用程序将(例如)读取子标准输出中的数据,并相应地更新 GUI。
OTOH,如果您有一个“全屏”DOS 程序,它是为直接使用屏幕缓冲区而编写的,那么将其保留为 GUI 中同一应用程序的一部分会更容易一些。在典型情况下,此类程序将具有一些代码来检索指向屏幕的指针,然后将其视为字符/属性对的二维数组。要“捕获”其输出,您可以用自己的数组代替硬件屏幕缓冲区。从那里你有两个选择。如果原始代码是用 C++(或与 C++ 兼容的 C)编写的,您的虚拟屏幕缓冲区可以重载某些运算符以通知您想要进行更改。否则,您可以每(例如)100 毫秒轮询一次,并(例如)散列内容以确定它是否已更改,因此您需要更新 GUI。虽然轮询听起来不像是一个好主意,但以 100 毫秒的间隔对 8K 数据进行哈希处理实际上不太可能导致重大问题。
不过我会重复一遍:至少在我看来,只有两种选择可能效果很好:要么通过根据需要重新架构来干净地集成代码,让它在线程中良好地运行,要么通过以下方式干净地分离代码:在计算和 GUI 之间构建清晰的通信。至少 IME,这些极端之间的中间点很少效果很好。您需要分离或集成代码,但无论哪种方式,您都需要彻底、干净地完成它。
Others have already pointed out that WM_PAINT is normally synthesized when needed, rather than there ever actually being a WM_PAINT message in the message queue.
Getting to your real problem, it seems to me that there are really only two real possibilities: either re-architect it, or leave it alone. If you're going to leave it alone, it's probably best to just isolate it, so your application runs in two almost completely separate pieces: one is your nearly-unchanged DOS application, doing things just as it always has. The other is a GUI front end that's almost completely separate.
Exactly how you do that depends on how the DOS application produces its output. If it uses standard streams (e.g., writing to standard output with printf and such) it's probably easiest to convert it to a Win32 console application. Have your GUI application spawn the console application, and redirect its standard input, standard output and standard error streams into anonymous pipes to the parent. The parent GUI application will then (for example) read data as it comes in on the child's standard output, and update your GUI accordingly.
If, OTOH, you have a "full screen" DOS program that was written to use the screen buffer directly, then it's a bit easier to keep it as part of the same application in the GUI. In a typical case, such a program will have some code that retrieves a pointer to the screen, and then treats that as a 2D array of character/attribute pairs. To "trap" its output, you substitute an array of your own in place of the hardware screen buffer. From there you have two choices. If the original code is written in C++ (or C that's compatible with C++) your virtual screen buffer can overload some operators to notify you want changes take place. Otherwise, you can poll it every (say) 100 ms, and (for example) hash the contents to determine whether it's changed so you need to update your GUI. While polling never sounds like a good idea, hashing 8K of data at 100 ms intervals isn't really likely to cause a major problem.
I'll repeat though: at least IMO, there are only two choices that are likely to work out well: Either cleanly integrate the code by re-architecting as needed to let it run nicely in a thread, or else cleanly separate the code by architecting a clean communication between the computation and the GUI. At least IME, halfway points between those extremes rarely work out well. You need to either separate or integrate the code, but either way you need to do it thoroughly and cleanly.