Windows的对话框是获取信息输入的主要手段,增加线程则是获得更好UI响应的重要方式。由于Windows在处理对话框时进行线程调度的特殊性,如果不对此加以特别注意,增加线程可能不能带来UI响应的改善。
1 跨线程创建对话框
1.1 需求
有这样的应用场景:创建非模态对话框后需要马上做些耗时的工作,而同时又希望能够立刻在对话框上操作,所以,希望让非模态对话框工作在单独的线程上。
1.2 方案
主线程启动一个UI线程,并且,让这个线程创建非模态对话框。
想法是:既然用单独的线程创建了对话框,所以,主线程在创建UI线程后,就可以继续自己其他耗时的工作了。
1.3 实现
用一个简单的例子程序来试验这个方案:在VC++中创建一个基于SDI的MFC应用程序,在其CView派生类中处理“Call”菜单命令,创建派生自CWinThread的UI线程,在这个UI线程初始化过程中创建一个对话框。
1.3.1 单独的UI线程创建对话框
实现一个派生自CWinThread的UI线程类,其中最关键的是CreateMyDlg和DestroyMyDlg函数。
类如下
class UIWorker : public CWinThread
{
DECLARE_DYNCREATE(UIWorker)
protected:
UIWorker(); // protected constructor used by dynamic creation
// Attributes
public:
// Operations
public:
bool CreateMyDlg( void );
void SetOwnerWnd( HWND hWnd );
void DestroyMyDlg( void );
……
private:
CDlgUserTest* m_pDlgTest;
HWND m_hOwnerWnd;
};
CreateMyDlg和DestroyMyDlg的实现都非常简单
bool UIWorker::CreateMyDlg( void )
{
m_pDlgTest = new CDlgUserTest;
if ( NULL == m_pDlgTest )
{
return false;
}
CWnd* pWnd = NULL;
if ( NULL != m_hOwnerWnd )
{
pWnd = reinterpret_cast<CWnd*>( CWnd::FromHandle( m_hOwnerWnd ));
if ( NULL == pWnd )
{
return false;
}
}
BOOL bSuccess = m_pDlgTest->Create( IDD_DIALOG_TEST, pWnd );
if ( bSuccess )
{
bSuccess = m_pDlgTest->ShowWindow( SW_SHOW );
}
return bSuccess?true:false;
}
void UIWorker::DestroyMyDlg( void )
{
if ( NULL != m_pDlgTest )
{
delete m_pDlgTest;
m_pDlgTest = NULL;
}
}
1.3.2 主线程创建UI线程
主线程就更加简单了。在菜单的对应操作中,创建线程,为了表示主线程继续工作,提供一个循环。
bool CUserView::Call(void)
{
m_pUIWorker = static_cast<UIWorker*>( AfxBeginThread(
RUNTIME_CLASS( UIWorker )));
for ( int i = 0; i < 100000; ++i )
for ( int j = 0; j < 10000; ++j )
;
return true;
}
1.4 结果:奇怪的延迟
希望达到的效果是:
主程序启动后,显示一个单文档视界面,有一个Work菜单
点击Call菜单后,对话框应该马上弹出,显示为:
执行中,对话框不会马上弹出,而会等待一定的时间,直到循环结束,CUserView的Call函数返回,对话框才会弹出。等待的时间和循环的长短成正比。
2 问题分析
2.1 不单纯的对话框:要求Windows作特殊处理
对话框是一种很不单纯的窗口。无论是创建、消息分发还是销毁,Windows都会对对话框做一些特殊的处理。如果用SDK进行对话框编程,就会发现创建对话框需要专门的Win32 API。而且,我们查阅平台SDK的讲述时,也会发现对话框需要Windows进行若干额外的“照顾”。事实上,之所以会出现前述的“延迟”情况,就是Windows进行额外协调的结果。
2.2 窗口协调导致等待
2.2.1 Windows协调对话框弹出过程
使用SPY++研究窗口消息,会发现非模态对话框创建时,原来拥有焦点的窗口会收到WM_KILLFOCUS消息,而且获得焦点的窗口是创建的对话框。
2.2.2 线程需要分发消息,不能堵塞
根据例子来看,这个窗口焦点的协调过程被上升到了线程协调的层次。现象就是:如果被去激活的窗口的线程被阻塞,不能立刻处理WM_KILLFOCUS消息的话,创建对话框的线程也会被阻塞,对话框一直不能被显示出来,直到线程不再阻塞,WM_KILLFOCUS被分发和处理。
2.3 解决方案
这个问题产生的原因是:在主线程繁忙的时候有主线程必须要处理的消息,也就是主线程消息循环因为窗口处理函数占用时间过长而被阻塞。因此,这个问题更多是一个设计问题而非技术难点。也许,我们该问的是:
我真的要用一个冗长的工作来阻塞主线程这样长的时间吗?
我是否可以在单独的一个工作者线程中来处理这个长的工作?
考虑了其他的可能性后,如果对上述问题的答案仍然为“是”的话,可以采取以下解决方案:因为我们缺少的是消息循环,所以,加上消息循环,让对话框能够显示出来之后,再去进行其他工作的处理。
bool CUserView::Call(void)
{
m_pUIWorker = static_cast<UIWorker*>( AfxBeginThread(
RUNTIME_CLASS( UIWorker )));
while ( !m_pUIWorker->GetDoneFlag())
{
MSG msg;
::GetMessage( &msg, this->m_hWnd, NULL, NULL );
::TranslateMessage( &msg );
::DispatchMessage( &msg );
}
for ( int i = 0; i < 100000; ++i )
for ( int j = 0; j < 10000; ++j )
;
return true;
}
红色的代码就是加上消息循环。要注意的是,相应的线程类里面应该在显示出对话框后设置一个标志,并且让主线程可以查询到这个标志,从而终止这个临时的消息循环。
3 启示
3.1 Windows线程调度
Windows的线程调度原则对于程序员来说非常简单。这条原则是:程序员无法决定线程调度过程。
只是因为“程序员无法决定线程调度过程”,并不意味着程序员不应该去了解一些特别的线程调度过程。在某些场合下,正如上面在和对话框相关的某些时机,也许,Windows的线程调度是有明确规则的。所以,大多数情况下,程序员可以认为Windows的线程调度对于自己来说是一个黑盒。但是,某些时候,这个盒子中间发生的事情也需要了解和掌握。
3.2 对话框
对话框是一个古老的话题,很多的人仔细讨论了对话框的方方面面。对话框一直是Windows里面非常特殊的一种窗口。它的消息循环,与其他窗口的协调要求,都和普通的窗口有不同之处。因此,为了配合这些不同之处,Windows在线程协调上也做了一些手脚。
3.3 多线程编程更多是一种设计
更重要的启示是:多线程需要更多的考虑设计。
毫无疑问,多线程可以使多种工作并行进行,提高工作效率,改善界面响应。然而,多线程应用中一个麻烦的问题是:决定对哪些工作使用单独的线程。这个决定过程其实就是设计过程。如果设计方案不合理,比如,如本例子中反映出来的问题——在主线程被长时间的工作阻塞的情况下,增加的线程并不会给我们带来明显的响应改善。而且,如果设计方案不合理,会带来更多的“临时机制”的采用,如本例中必须增加一个单独的消息循环,并且需要在两个线程中就对话框是否创建出来进行通讯。这样的实现在很大程度上削减了希望用多线程带来的好处。