Windows的對話框是獲取信息輸入的主要手段,增加線程則是獲得更好UI響應的重要方式。由于Windows在處理對話框時進行線程調度的特殊性,如果不對此加以特別注意,增加線程可能不能帶來UI響應的改善。

1         跨線程創(chuàng)建對話框

1.1      需求

有這樣的應用場景:創(chuàng)建非模態(tài)對話框后需要馬上做些耗時的工作,而同時又希望能夠立刻在對話框上操作,所以,希望讓非模態(tài)對話框工作在單獨的線程上。

1.2      方案

主線程啟動一個UI線程,并且,讓這個線程創(chuàng)建非模態(tài)對話框。

想法是:既然用單獨的線程創(chuàng)建了對話框,所以,主線程在創(chuàng)建UI線程后,就可以繼續(xù)自己其他耗時的工作了。

1.3      實現

用一個簡單的例子程序來試驗這個方案:在VC++中創(chuàng)建一個基于SDIMFC應用程序,在其CView派生類中處理“Call”菜單命令,創(chuàng)建派生自CWinThreadUI線程,在這個UI線程初始化過程中創(chuàng)建一個對話框。

1.3.1      單獨的UI線程創(chuàng)建對話框

實現一個派生自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      主線程創(chuàng)建UI線程

主線程就更加簡單了。在菜單的對應操作中,創(chuàng)建線程,為了表示主線程繼續(xù)工作,提供一個循環(huán)。

 

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菜單后,對話框應該馬上彈出,顯示為:

 

執(zhí)行中,對話框不會馬上彈出,而會等待一定的時間,直到循環(huán)結束,CUserViewCall函數返回,對話框才會彈出。等待的時間和循環(huán)的長短成正比。

2         問題分析

2.1      不單純的對話框:要求Windows作特殊處理

對話框是一種很不單純的窗口。無論是創(chuàng)建、消息分發(fā)還是銷毀,Windows都會對對話框做一些特殊的處理。如果用SDK進行對話框編程,就會發(fā)現創(chuàng)建對話框需要專門的Win32 API。而且,我們查閱平臺SDK的講述時,也會發(fā)現對話框需要Windows進行若干額外的“照顧”。事實上,之所以會出現前述的“延遲”情況,就是Windows進行額外協(xié)調的結果。

2.2      窗口協(xié)調導致等待

2.2.1      Windows協(xié)調對話框彈出過程

使用SPY++研究窗口消息,會發(fā)現非模態(tài)對話框創(chuàng)建時,原來擁有焦點的窗口會收到WM_KILLFOCUS消息,而且獲得焦點的窗口是創(chuàng)建的對話框。

2.2.2      線程需要分發(fā)消息,不能堵塞

根據例子來看,這個窗口焦點的協(xié)調過程被上升到了線程協(xié)調的層次。現象就是:如果被去激活的窗口的線程被阻塞,不能立刻處理WM_KILLFOCUS消息的話,創(chuàng)建對話框的線程也會被阻塞,對話框一直不能被顯示出來,直到線程不再阻塞,WM_KILLFOCUS被分發(fā)和處理。

2.3      解決方案

這個問題產生的原因是:在主線程繁忙的時候有主線程必須要處理的消息,也就是主線程消息循環(huán)因為窗口處理函數占用時間過長而被阻塞。因此,這個問題更多是一個設計問題而非技術難點。也許,我們該問的是:

n         我真的要用一個冗長的工作來阻塞主線程這樣長的時間嗎?

n         我是否可以在單獨的一個工作者線程中來處理這個長的工作?

考慮了其他的可能性后,如果對上述問題的答案仍然為“是”的話,可以采取以下解決方案:因為我們缺少的是消息循環(huán),所以,加上消息循環(huá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;

}

紅色的代碼就是加上消息循環(huán)。要注意的是,相應的線程類里面應該在顯示出對話框后設置一個標志,并且讓主線程可以查詢到這個標志,從而終止這個臨時的消息循環(huán)。

3         啟示

3.1      Windows線程調度

Windows的線程調度原則對于程序員來說非常簡單。這條原則是:程序員無法決定線程調度過程。

只是因為“程序員無法決定線程調度過程”,并不意味著程序員不應該去了解一些特別的線程調度過程。在某些場合下,正如上面在和對話框相關的某些時機,也許,Windows的線程調度是有明確規(guī)則的。所以,大多數情況下,程序員可以認為Windows的線程調度對于自己來說是一個黑盒。但是,某些時候,這個盒子中間發(fā)生的事情也需要了解和掌握。

3.2      對話框

對話框是一個古老的話題,很多的人仔細討論了對話框的方方面面。對話框一直是Windows里面非常特殊的一種窗口。它的消息循環(huán),與其他窗口的協(xié)調要求,都和普通的窗口有不同之處。因此,為了配合這些不同之處,Windows在線程協(xié)調上也做了一些手腳。

3.3      多線程編程更多是一種設計

更重要的啟示是:多線程需要更多的考慮設計。

毫無疑問,多線程可以使多種工作并行進行,提高工作效率,改善界面響應。然而,多線程應用中一個麻煩的問題是:決定對哪些工作使用單獨的線程。這個決定過程其實就是設計過程。如果設計方案不合理,比如,如本例子中反映出來的問題——在主線程被長時間的工作阻塞的情況下,增加的線程并不會給我們帶來明顯的響應改善。而且,如果設計方案不合理,會帶來更多的“臨時機制”的采用,如本例中必須增加一個單獨的消息循環(huán),并且需要在兩個線程中就對話框是否創(chuàng)建出來進行通訊。這樣的實現在很大程度上削減了希望用多線程帶來的好處。

  

發(fā)表于 @ 2007年09月17日 15:48:00|評論(1 )|編輯

新一篇: .NET垃圾收集器的過去現在和未來(一) | 舊一篇: Clear Type之父談閱讀革命(三,終結篇)

guogangj 發(fā)表于2009年3月4日 8:35:20  IP:舉報
今天才看到這篇文章,我在我的項目中遇到這么個問題:在load文件的時候,可能需要很多時間,因為我的界面如何顯示,有相當大一部分是根據load的內容的,甚至我需要根據load文件的內容加載不同的dll,所以我打算在load過程中做一個主窗口的模態(tài)對話框,上面寫上“Please Wait...”。但模態(tài)對話框會阻止主線程繼續(xù)往下執(zhí)行l(wèi)oad動作,所以我先單獨創(chuàng)建了一個線程,由這個線程創(chuàng)建模態(tài)對話框,就出現了此文中的“奇怪的延遲”而達不到效果,當然我的解決方法和你的不太相同,我使用了一個Timer和一個Feedback消息來實現的……具體就不說了。