工作线程如何与主UI线程通信?

5

工作线程与主 UI 线程之间通信的最佳方式是什么?

总结:我的 C++/MFC 应用程序基于对话框开发。为了进行冗长的计算,主 UI 线程创建了多个工作线程。随着工作线程在计算中的进展,它们向主 UI 线程报告其进度,然后主 UI 线程显示进度。

对于在共享内存中的数字进度值,这可以正常工作(由工作线程写入,由 UI 读取),但是我在文本进度消息方面遇到了麻烦。我的尝试解决方案已经经历了多次迭代,但似乎都不起作用。

  1. 我让 UI 线程向工作线程传递控件指针,然后工作线程直接更新 UI。这样并不是很有效,并且似乎是错误的方法。

  2. 我让工作线程通过 SendMessage 向 UI 线程的窗口发送消息。这会导致死锁。(SendMessage 直到消息被处理才会返回。)

  3. 与第2种相同,只是使用 PostMessage 向 UI 线程的窗口发送消息。这在一段时间内有效,但是消息丢失了。(PostMessage 立即返回。)进一步调查发现,消息队列的配额(默认为10,000)已超过。

  4. 我增加了消息队列的配额(在注册表中变量 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\USERPostMessageLimit),但丢失的消息数量没有改变。

  5. 我让每个工作线程将消息缓存在4 KByte 的缓冲区中,并在缓冲区填满时使用 PostMessage。这种方法失败了,因为 UI 线程从未收到任何消息。当我将缓冲区大小增加到64 KBytes 时仍然是如此。

工作线程以“最低”优先级运行,UI 线程以“正常”优先级运行。工作线程使用以下代码发送消息:

UIMessage *pUI=new UIMessage; // so it won't go out of scope (main dialog will delete it)
pUI->buffer=traceLineBuffer; pUI->nOutputs=traceN;
BOOL ok=::PostMessage(hWndMainDlg,TraceLineMsg,(WPARAM)pUI, NULL/*lParam*/);

UI通过类似以下代码接收它们:

BEGIN_MESSAGE_MAP(CMainDlg, CDialog)
...
ON_MESSAGE(TraceLineMsg,OnTraceLineMsg)
...
END_MESSAGE_MAP()

LRESULT CMainDlg::OnTraceLineMsg(WPARAM wParam, LPARAM lParam)
{
    UIMessage *pUI=(UIMessage *)wParam;
    char *p=pUI->buffer;
    // PROCESS BUFFER
    delete[] pUI->buffer;
    delete pUI;
    return 0;
}

问题:

  1. 在可能出现数千个文本报告的突发情况下,工作人员发布进度报告的首选方式是什么?

  2. 为什么我不能增加队列中发布消息的配额?

  3. 为什么主 UI 线程似乎从未接收到缓冲区中的消息,即使传输它们的机制与发布单个报告相同?

64 位 Windows 7、Visual Studio 2010、本机 C++/MFC


发布消息是一种方法。但你有没有考虑大大降低消息数量?为什么不只更新每100或1000条消息? - Mike Makuch
1
你所采取的方法是正确的,但是发送足够多的消息以超过队列限制实际上没有任何意义。可能导致发布的消息未被接收的唯一原因是超出了队列限制或者主线程被阻塞,例如在Sleep或WaitFor...调用中。当这些消息被发布时,主线程在做什么?您还分配了另一个traceLineBuffer与每个消息一起使用吗? - ScottMcP-MVP
koodawg:我通过在(5)中缓冲它们来减少消息数量。 - Woody20
ScottMcP-MVP: 主线程在等待多个对象,直到工作线程完成。也许这就是问题所在。是的,在每个消息中都会分配一个新的消息和缓冲区,正如您可以从我的OnTraceLineMsg中删除它们一样。 - Woody20
我之前应该明确指出,WaitForMultipleObjects函数是用来等待工作线程完成的:WaitForMultipleObjects(nThreadsActive, threadHandles, TRUE/*等待所有线程都完成*/, INFINITE); - Woody20
3个回答

4

当主线程处于WaitForMultipleObjects调用中时,不会处理任何消息,并且无法更新任何控件或其他窗口。解决方法是:不要这样做。


2

在Windows操作系统上,MFC工作线程与主线程进行通信有多种选择。你可以使用标准的线程信号和同步原语(互斥锁、信号量、事件),也可以使用易于使用的PostMessage,以及性能更高的I/O完成端口机制。

// syncronization
{
    CSingleLock lock(&sharedCriticalSection,TRUE);
    sharedList.push_back(msg);
}
// other thread(s) are blocked/pending or you send an event or message to signal

// messages
Data* data = new Data(payload);
PostMessage(hWnd, REGISTERED_MESSAGE, 0, (LPARAM)data);
// target window handles message and deletes data 
// if it is not blocked or too slow and the queue overflows

// skipping lots of IO completion port boilerplate and showing the key methods
messagePort = CreateIoCompletionPort(...);
...
GetQueuedCompletionStatus(messagePort,...);
...
PostQueuedCompletionStatus(messagePort,...);

如果您阻塞或忙等待线程完成,那么它们都无法完成太多工作或提高性能或响应能力。

对您的观察进行评论:

  1. 不要让工作线程触摸GUI。
  2. 不要从工作线程使用SendMessage。
  3. 只要UI可以跟上并且没有被阻塞,PostMessage就可以用于低容量。
  4. 如果您认为需要更改这一点,则应重新考虑您的解决方案。发送更少的消息或使用更高效的选项。
  5. 如果UI没有被阻塞,合并消息可能会有所帮助。

回答您的问题:

  1. 重新评估是否真的需要在短时间内发送数千条消息。用户每秒需要多少次更改?如果需要全部发送,请查看I/O完成端口机制。
  2. 我不会尝试,但它可能有效,除非...
  3. 您的主UI线程在WaitForMultipleObjects中被阻塞等待工作线程完成,并且在阻塞时,您的工作线程和其他事件必须生成超过最大队列消息。

2
在GUI进度报告被用户吸收之前,发布比这更快的进度报告没有太多意义。当其他线程中有大量活动时,常见的做法是使用GUI定时器轮询线程中的进度变量,并更新GUI控件,例如每500毫秒更新一次。
这是非常少数几次轮询定时器实际上具有优势的情况之一。您可以获得定期的进度报告,而不会使GUI窗口消息队列充斥着更新。例如,uTorrent客户端(其中有大量网络活动)使用此方案 - 尝试在收到每个网络协议单元时更新GUI下载统计信息肯定会使GUI崩溃。
在第5点中,您的缓冲方案应该有效。我经常通过将对象指针加载到LPARAM或WPARAM中将大型数据项传输到主GUI线程中,在工作线程中new它们,在显示后在GUI中delete'ing它们。你的第5个方法应该可行,至少减少了进度数据传输的开销。我只能认为要显示的数据量仍然过大,因此GUI线程仍无法跟上:(

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接