Windows的对话框是获取信息输入的主要手段,增加线程则是获得更好UI响应的重要方式。由于Windows在处理对话框时进行线程调度的特殊性,如果不对此加以特别注意,增加线程可能不能带来UI响应的改善。

1         跨线程创建对话框

1.1      需求

有这样的应用场景:创建非模态对话框后需要马上做些耗时的工作,而同时又希望能够立刻在对话框上操作,所以,希望让非模态对话框工作在单独的线程上。

1.2      方案

主线程启动一个UI线程,并且,让这个线程创建非模态对话框。

想法是:既然用单独的线程创建了对话框,所以,主线程在创建UI线程后,就可以继续自己其他耗时的工作了。

1.3      实现

用一个简单的例子程序来试验这个方案:在VC++中创建一个基于SDIMFC应用程序,在其CView派生类中处理“Call”菜单命令,创建派生自CWinThreadUI线程,在这个UI线程初始化过程中创建一个对话框。

1.3.1      单独的UI线程创建对话框

实现一个派生自CWinThreadUI线程类,其中最关键的是CreateMyDlgDestroyMyDlg函数。

类如下

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;

};

 

CreateMyDlgDestroyMyDlg的实现都非常简单

 

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菜单后,对话框应该马上弹出,显示为:

 

执行中,对话框不会马上弹出,而会等待一定的时间,直到循环结束,CUserViewCall函数返回,对话框才会弹出。等待的时间和循环的长短成正比。

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      解决方案

这个问题产生的原因是:在主线程繁忙的时候有主线程必须要处理的消息,也就是主线程消息循环因为窗口处理函数占用时间过长而被阻塞。因此,这个问题更多是一个设计问题而非技术难点。也许,我们该问的是:

n         我真的要用一个冗长的工作来阻塞主线程这样长的时间吗?

n         我是否可以在单独的一个工作者线程中来处理这个长的工作?

考虑了其他的可能性后,如果对上述问题的答案仍然为“是”的话,可以采取以下解决方案:因为我们缺少的是消息循环,所以,加上消息循环,让对话框能够显示出来之后,再去进行其他工作的处理。

 

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      多线程编程更多是一种设计

更重要的启示是:多线程需要更多的考虑设计。

毫无疑问,多线程可以使多种工作并行进行,提高工作效率,改善界面响应。然而,多线程应用中一个麻烦的问题是:决定对哪些工作使用单独的线程。这个决定过程其实就是设计过程。如果设计方案不合理,比如,如本例子中反映出来的问题——在主线程被长时间的工作阻塞的情况下,增加的线程并不会给我们带来明显的响应改善。而且,如果设计方案不合理,会带来更多的“临时机制”的采用,如本例中必须增加一个单独的消息循环,并且需要在两个线程中就对话框是否创建出来进行通讯。这样的实现在很大程度上削减了希望用多线程带来的好处。

  

发表于 @ 2007年09月17日 15:48:00|评论(1 )|编辑

新一篇: .NET垃圾收集器的过去现在和未来(一) | 旧一篇: Clear Type之父谈阅读革命(三,终结篇)

guogangj 发表于2009年3月4日 8:35:20  IP:举报
今天才看到这篇文章,我在我的项目中遇到这么个问题:在load文件的时候,可能需要很多时间,因为我的界面如何显示,有相当大一部分是根据load的内容的,甚至我需要根据load文件的内容加载不同的dll,所以我打算在load过程中做一个主窗口的模态对话框,上面写上“Please Wait...”。但模态对话框会阻止主线程继续往下执行load动作,所以我先单独创建了一个线程,由这个线程创建模态对话框,就出现了此文中的“奇怪的延迟”而达不到效果,当然我的解决方法和你的不太相同,我使用了一个Timer和一个Feedback消息来实现的……具体就不说了。