關於“UI執行緒”

Max Woods發表於2014-11-09

http://www.cppblog.com/Streamlet/archive/2013/05/05/199999.html

 

緣起

這是一篇找噴的文章。

 

由於一些歷史原因和人際淵源,周圍同事談論一些技術話題的時候,經常使用“UI執行緒”一詞。雖然我從來沒有看到其確切定義,但心裡對其含義可能略懂,因此一直裝作心知肚明的樣子(以免被嘲諷)。

 

日前,一同事發了封郵件大談“UI執行緒”的概念,分享到大部門。大部門裡除了我們一個Windows客戶端部門,其他都是做網站的Java開發。因此,在他們面前談論一些我們並不成熟甚至並不存在的概念,有那麼一點點故弄玄虛的味道,這激起了我談論這個話題的小小慾望。當然,並不是說那封郵件裡說的有錯誤,事實上絕大部分語句都是正確的。不過我看到的最讓人豁然開朗的一句話卻是“UI執行緒並不是官方概念”。在此,我想梳理下有關“UI執行緒”始末和自己理解,望CppBlog的看官們批判。

 

 

對了,說明一下,本文的大背景是Win32桌面程式開發,.Net請繞道,WinRT請繞道,Web請繞道,手機請繞道……

 

UI執行緒”語源

據考證,“UI執行緒”的概念最早可能是在MFC中被引入的。目前能找到的官方提法是在:

http://msdn.microsoft.com/en-us/library/b807sta6(v=vs.110).aspx

 

MFCAfxBeginThread提供了兩個版本:

 

CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc,

                                    LPVOID pParam,

                                    int nPriority = THREAD_PRIORITY_NORMAL,

                                    UINT nStackSize = 0,

                                    DWORD dwCreateFlags = 0,

                                    LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);

 

CWinThread* AFXAPI AfxBeginThread(CRuntimeClass* pThreadClass,

                                    int nPriority = THREAD_PRIORITY_NORMAL,

                                    UINT nStackSize = 0,

                                    DWORD dwCreateFlags = 0,

                                    LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);

 

 

第一個版本用來讓人建立“工作執行緒”,第二個版本讓人用來建立“UI執行緒”。可能由於來自MFC的遠古光環,讓“UI執行緒”的提法略有普及。但除此之外,在Windows開發方面,似乎找不到第二個例子了。(如果有,請在評論中告訴我。)不管怎樣,既然MFC官方文件裡說了,那麼在“MFC領域”使用“UI執行緒”的提法總是可以的。下面,我們先來認識一下MFC中的UI執行緒以及工作執行緒。

 

MFC中的UI執行緒

我們按照http://msdn.microsoft.com/en-us/library/b807sta6(v=vs.110).aspx的指示,來建立一個“UI執行緒”。首先,繼承CWinThread

 

class CMyThread : public CWinThread

{

    DECLARE_DYNCREATE(CMyThread) 

 

public:

    virtual BOOL InitInstance()

    {

        return TRUE;

    }

};

 

IMPLEMENT_DYNCREATE(CMyThread, CWinThread)

 

然後,隨便找個地方來啟動執行緒:

 

AfxBeginThread(RUNTIME_CLASS(CMyThread));

 

執行緒被建立後,就處於CWinThread::Run裡的訊息迴圈之中了。來看看CWinThread::Run的實現:

 

// main running routine until thread exits

int CWinThread::Run()

{

    ASSERT_VALID(this);

    _AFX_THREAD_STATE* pState = AfxGetThreadState();

 

    // for tracking the idle time state

    BOOL bIdle = TRUE;

    LONG lIdleCount = 0;

 

    // acquire and dispatch messages until a WM_QUIT message is received.

    for (;;)

    {

        // phase1: check to see if we can do idle work

        while (bIdle &&

             !::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE))

        {

             // call OnIdle while in bIdle state

             if (!OnIdle(lIdleCount++))

                 bIdle = FALSE; // assume "no idle" state

        }

 

        // phase2: pump messages while available

        do

        {

             // pump message, but quit on WM_QUIT

             if (!PumpMessage())

                 return ExitInstance();

 

             // reset "no idle" state after pumping "normal" message

             //if (IsIdleMessage(&m_msgCur))

             if (IsIdleMessage(&(pState->m_msgCur)))

             {

                 bIdle = TRUE;

                 lIdleCount = 0;

             }

 

        } while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE));

    }

}

 

粗粗看一下,是個夾雜了OnIdle概念的訊息迴圈。

 

再看一下AfxBeginThread

 

CWinThread* AFXAPI AfxBeginThread(CRuntimeClass* pThreadClass,

    int nPriority, UINT nStackSize, DWORD dwCreateFlags,

    LPSECURITY_ATTRIBUTES lpSecurityAttrs)

{

#ifndef_MT

    pThreadClass;

    nPriority;

    nStackSize;

    dwCreateFlags;

    lpSecurityAttrs;

 

    return NULL;

#else

    ASSERT(pThreadClass != NULL);

    ASSERT(pThreadClass->IsDerivedFrom(RUNTIME_CLASS(CWinThread)));

 

    CWinThread* pThread = (CWinThread*)pThreadClass->CreateObject();

    if (pThread == NULL)

        AfxThrowMemoryException();

    ASSERT_VALID(pThread);

 

    pThread->m_pThreadParams = NULL;

    if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,

        lpSecurityAttrs))

    {

        pThread->Delete();

        return NULL;

    }

    VERIFY(pThread->SetThreadPriority(nPriority));

    if (!(dwCreateFlags & CREATE_SUSPENDED))

    {

        ENSURE(pThread->ResumeThread() != (DWORD)-1);

    }

 

    return pThread;

#endif//!_MT

}

 

其中呼叫了CWinThread::CreateThread

 

BOOL CWinThread::CreateThread(DWORD dwCreateFlags, UINT nStackSize,

    LPSECURITY_ATTRIBUTES lpSecurityAttrs)

{

#ifndef_MT

    dwCreateFlags;

    nStackSize;

    lpSecurityAttrs;

 

    return FALSE;

#else

    ENSURE(m_hThread == NULL);  // already created?

 

    // setup startup structure for thread initialization

    _AFX_THREAD_STARTUP startup; memset(&startup, 0, sizeof(startup));

    startup.pThreadState = AfxGetThreadState();

    startup.pThread = this;

    startup.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);

    startup.hEvent2 = ::CreateEvent(NULL, TRUE, FALSE, NULL);

    startup.dwCreateFlags = dwCreateFlags;

    if (startup.hEvent == NULL || startup.hEvent2 == NULL)

    {

        TRACE(traceAppMsg, 0, "Warning: CreateEvent failed in CWinThread::CreateThread.\n");

        if (startup.hEvent != NULL)

             ::CloseHandle(startup.hEvent);

        if (startup.hEvent2 != NULL)

             ::CloseHandle(startup.hEvent2);

        return FALSE;

    }

 

    // create the thread (it may or may not start to run)

    m_hThread = (HANDLE)(ULONG_PTR)_beginthreadex(lpSecurityAttrs, nStackSize, 

        &_AfxThreadEntry, &startup, dwCreateFlags | CREATE_SUSPENDED, (UINT*)&m_nThreadID);

    if (m_hThread == NULL)

    {

        ::CloseHandle(startup.hEvent);

        ::CloseHandle(startup.hEvent2);

        return FALSE;

    }

 

    // start the thread just for MFC initialization

    VERIFY(ResumeThread() != (DWORD)-1);

    VERIFY(::WaitForSingleObject(startup.hEvent, INFINITE) == WAIT_OBJECT_0);

    ::CloseHandle(startup.hEvent);

 

    // if created suspended, suspend it until resume thread wakes it up

    if (dwCreateFlags & CREATE_SUSPENDED)

        VERIFY(::SuspendThread(m_hThread) != (DWORD)-1);

 

    // if error during startup, shut things down

    if (startup.bError)

    {

        VERIFY(::WaitForSingleObject(m_hThread, INFINITE) == WAIT_OBJECT_0);

        ::CloseHandle(m_hThread);

        m_hThread = NULL;

        ::CloseHandle(startup.hEvent2);

        return FALSE;

    }

 

    // allow thread to continue, once resumed (it may already be resumed)

    VERIFY(::SetEvent(startup.hEvent2));

    return TRUE;

#endif//!_MT

}

 

執行緒函式為_AfxThreadEntry

 

UINT APIENTRY _AfxThreadEntry(void* pParam)

{

    _AFX_THREAD_STARTUP* pStartup = (_AFX_THREAD_STARTUP*)pParam;

    ASSERT(pStartup != NULL);

    ASSERT(pStartup->pThreadState != NULL);

    ASSERT(pStartup->pThread != NULL);

    ASSERT(pStartup->hEvent != NULL);

    ASSERT(!pStartup->bError);

 

    CWinThread* pThread = pStartup->pThread;

    CWnd threadWnd;

    TRY

    {

        // inherit parent's module state

        _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();

        pThreadState->m_pModuleState = pStartup->pThreadState->m_pModuleState;

 

        // set current thread pointer for AfxGetThread

        AFX_MODULE_STATE* pModuleState = AfxGetModuleState();

        pThread->m_pModuleState = pModuleState;

        AFX_MODULE_THREAD_STATE* pState = pModuleState->m_thread;

        pState->m_pCurrentWinThread = pThread;

 

        // forced initialization of the thread

        AfxInitThread();

 

        // thread inherits app's main window if not already set

        CWinApp* pApp = AfxGetApp();

        if (pApp != NULL &&

             pThread->m_pMainWnd == NULL && pApp->m_pMainWnd->GetSafeHwnd() != NULL)

        {

             // just attach the HWND

             threadWnd.Attach(pApp->m_pMainWnd->m_hWnd);

             pThread->m_pMainWnd = &threadWnd;

        }

    }

    CATCH_ALL(e)

    {

        // Note: DELETE_EXCEPTION(e) not required.

 

        // exception happened during thread initialization!!

        TRACE(traceAppMsg, 0, "Warning: Error during thread initialization!\n");

 

        // set error flag and allow the creating thread to notice the error

        threadWnd.Detach();

        pStartup->bError = TRUE;

        VERIFY(::SetEvent(pStartup->hEvent));

        AfxEndThread((UINT)-1, FALSE);

        ASSERT(FALSE);  // unreachable

    }

    END_CATCH_ALL

 

    // pStartup is invlaid after the following

    // SetEvent (but hEvent2 is valid)

    HANDLE hEvent2 = pStartup->hEvent2;

 

    // allow the creating thread to return from CWinThread::CreateThread

    VERIFY(::SetEvent(pStartup->hEvent));

 

    // wait for thread to be resumed

    VERIFY(::WaitForSingleObject(hEvent2, INFINITE) == WAIT_OBJECT_0);

    ::CloseHandle(hEvent2);

 

    // first -- check for simple worker thread

    DWORD nResult = 0;

    if (pThread->m_pfnThreadProc != NULL)

    {

        nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams);

        ASSERT_VALID(pThread);

    }

    // else -- check for thread with message loop

    else if (!pThread->InitInstance())

    {

        ASSERT_VALID(pThread);

        nResult = pThread->ExitInstance();

    }

    else

    {

        // will stop after PostQuitMessage called

        ASSERT_VALID(pThread);

        nResult = pThread->Run();

    }

 

    // cleanup and shutdown the thread

    threadWnd.Detach();

    AfxEndThread(nResult);

 

    return 0;   // not reached

}

 

林林總總地貼了這麼些程式碼,差不多可以看出MFCCWinThread的一些實現機制了。總的來說,MFC提供的“UI執行緒”,預設為執行緒實現了一個帶OnIdle機制的訊息迴圈,同時,它Attach了應用程式主視窗,m_pMainWindow被設為了應用程式主視窗,它在OnIdle以及ProcessMessageFilter中被用到。

 

注意到在_AfxThreadEntry中有一行AfxInitThread,這裡面註冊了一個訊息鉤子,鉤子回撥函式裡面會呼叫ProcessMessageFilter。當處於幫助模式的時候,這個函式會向m_pMainWindow傳送codeID_HELPWM_COMMAND訊息。

 

MFC中的工作執行緒

工作執行緒由另一個AfxBeginThread啟動:

 

CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,

    int nPriority, UINT nStackSize, DWORD dwCreateFlags,

    LPSECURITY_ATTRIBUTES lpSecurityAttrs)

{

#ifndef_MT

    pfnThreadProc;

    pParam;

    nPriority;

    nStackSize;

    dwCreateFlags;

    lpSecurityAttrs;

 

    return NULL;

#else

    ASSERT(pfnThreadProc != NULL);

 

    CWinThread* pThread = DEBUG_NEW CWinThread(pfnThreadProc, pParam);

    ASSERT_VALID(pThread);

 

    if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,

        lpSecurityAttrs))

    {

        pThread->Delete();

        return NULL;

    }

    VERIFY(pThread->SetThreadPriority(nPriority));

    if (!(dwCreateFlags & CREATE_SUSPENDED))

        VERIFY(pThread->ResumeThread() != (DWORD)-1);

 

    return pThread;

#endif//!_MT)

}

 

它呼叫了CWinThread的如下建構函式:

 

CWinThread::CWinThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam)

{

    m_pfnThreadProc = pfnThreadProc;

    m_pThreadParams = pParam;

 

    CommonConstruct();

}

 

然後同樣用CWinThread::CreateThread建立執行緒。新執行緒的入口函式同樣為_AfxThreadEntry。與上例不同,這時,程式進入這個if判斷的第一個分支:

 

    // first -- check for simple worker thread

    DWORD nResult = 0;

    if (pThread->m_pfnThreadProc != NULL)

    {

        nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams);

        ASSERT_VALID(pThread);

    }

    // else -- check for thread with message loop

    else if (!pThread->InitInstance())

    {

        ASSERT_VALID(pThread);

        nResult = pThread->ExitInstance();

    }

    else

    {

        // will stop after PostQuitMessage called

        ASSERT_VALID(pThread);

        nResult = pThread->Run();

    }

 

直接呼叫我們傳入的執行緒函式,而不再進入CWinThread::Run。這裡,m_pMainWindow的處理與上例相同。

 

MFC中的UI執行緒與工作執行緒的異同

綜上,我們可以看到,MFC裡的UI執行緒裡,CWinThread實現了一個訊息迴圈,這是工作執行緒所不具備的。除此之外,差異之處很寥寥。從MFC程式碼裡來看,MFC的開發者對兩者的稱呼只是“simple worker thread”和“thread with message loop”,事實上兩者的程式碼層面的區別也正是如此。並且,CWinThread::Run被宣告為虛的,這意味著我們可以覆蓋它——同時在自己的版本里不實現訊息迴圈。

 

MSDN裡,將兩個_AfxBeginThread的使用分別稱為建立“User Interface Thread”和建立“Worker Thread”。

 

嘗試定義“UI執行緒”

現在開始,我們走出MFC,回到通用程式領域。看看“UI執行緒”是否有必要定義,以及應該如何定義。

 

首先有一點要明白,在MFC之外,UI執行緒的官方概念已經不存在了。這時,你去問一個人“你知道什麼是UI執行緒嗎?”是很奇怪很愚蠢的事情。如果他說不知道,你會怎麼做?你大概會告訴他你心中的定義,這表明你試圖讓他相信你心中的定義是真理(業界通用說法),並且不指定適用範圍(比如MFC內)。這是不道德的。

 

就像有一次,一位同事“嘲諷”我說:“Cookies是程式內全域性共享的,你不知道嗎?”我當然不知道呢,Cookies不是HTTP協議裡一行文字而已嗎?我願意怎麼處理就怎麼處理嘛,願意讓它在程式內全域性共享它就是程式內全域性共享的了,我不願意讓它在程式內全域性共享它就不是程式內全域性共享的了,不是嗎?後來,才知道他說的是“在WinINet中,Cookies是程式內全域性共享的”。

 

既然我們使用了WinINet,那麼有時候省略“在WinINet中”的限定或許是情有可原的。但我們如果沒有使用MFC,那麼不帶前提地大談“UI執行緒”就顯得不太合適了。稍稍總結下,我們談到“UI執行緒”一般是這些場景:

  1. 1.         不要在UI執行緒中做長時間的操作
  2. 2.         只能在UI執行緒中操作HWND
  3. 3.         我們搞個雙UI執行緒

 

第三點暫時無視吧,個人覺得無意義,什麼單UI執行緒、雙UI執行緒,這個在系統層面根本沒這個提法以及限制,完全取決於開發者。倒是前兩者,是有那麼一點意義的提法。為了描述方便,在本節中,暫且定義“UI執行緒”為,具有訊息迴圈,並且在其中至少建立了一個可見視窗的執行緒。(這裡可能有人會問,沒有視窗,你需要訊息迴圈幹嘛?一個例子:帶TimerProcTimer需要訊息迴圈。)

 

對於第一條,其實我並無多大異議,只要不去拷問別人什麼叫“UI執行緒”,這樣輕描淡寫的提及,大家總是心知肚明的。第二條是創造“UI執行緒”概念的一個的很大的使用場景,然而這個命題本身卻是錯誤的。起碼,SendMessagePostMessage是無論在哪個執行緒執行都是可行的,那麼,那一大堆由GUI庫包裝SendMessage而成的視窗操作函式自然也是隨處可用的,以及本質上由SendMessage實現的一些視窗API也是隨處可用的。為了說清楚此問題,好像要總結一下哪些API需要在UI執行緒中執行,以及哪些不需要——但這是不可能的,下一節我略舉幾個例子。

 

本節結束之前,我想將上述12改個提法,以避擴音及“UI執行緒”:

1.     不要在視窗回撥函式中做長時間的操作

2.     只能在建立HWND的執行緒中操作HWND

 

只能在建立HWND的執行緒中操作HWND

正例:

l  DestroyWindow MSDN Remark 中特意指明:A thread cannot use DestroyWindow to destroy a window created by a different thread.

 

反例:

l  SendMessagePostMessage 這兩個當然不必在建立視窗的執行緒使用了。

l  ShowWindowAsnyc這個函式的用途是“Sets the show state of a window created by a different thread”,雖然文件中沒特異指出可以在非建立執行緒中使用,但簡單腦補一下就知道可以。注意,這並不意味著ShowWindow一定需要在建立視窗的執行緒中使用。ShowWindowAsync做的是“posts a show-window event to the message queue of the given window”,它的意義在於“to avoid becoming nonresponsive while waiting for a nonresponsive application to finish processing a show-window event”。

l  GetWindowThreadProcessId用途:“Retrieves the identifier of the thread that created the specified window and, optionally, the identifier of the process that created the window”。同上,腦補。

 

精力有限,例子就不再舉了(各位可以幫忙補充)。按筆者個人理解,文件中沒有特意指出一定要在建立視窗的執行緒中使用的,一般是可以在其他執行緒甚至其他程式使用的。再來看一下SetWindowPos的一段Remark

As part of the Vista re-architecture, all services were moved off the interactive desktop into Session 0. hwnd and window manager operations are only effective inside a session and cross-session attempts to manipulate the hwnd will fail.

 

這裡它特別指出Vista之後,跨Session操作HWND會失敗。這從側面表明,跨執行緒玩一下HWND,或者跨程式玩一下,通常是不會失敗的。如果失敗了,那才是特例,就像DestroyWindow

 

通則、區域性規則、家長式規則

從上面的反例,我們可以知道,“只能在建立HWND的執行緒中操作HWND”這一命題是不成立的。它肯定不是Windows開發領域的通則。那麼,這句話從何而來呢?確切地說,我不知道。

 

第一,它可能是某個GUI庫的規則。可能因為這個GUI庫的設計問題,導致必須在建立HWND的執行緒中操作HWND。這句話或許因此成為某些GUI庫的區域性規則。但可能因為我們在交流的時候,有意無意地忽略了前提條件(“在MFC中”、“在WinINet中”),導致被誤解為通則。

 

第二,有可能有些前輩高人對於後輩跨執行緒操作HWND導致的一些問題感到厭倦,於是就對他們諄諄教誨:“孩子啊,只能在建立HWND的執行緒中操作HWND的。”然後世事變幻,滄海桑田,幾代以後,這句為防止不太會的人用錯的家長式規則被口口相傳當成了通則。

 

順便說一句,剛才的第一條不是說不要在“UI執行緒”中做長時間操作麼?那麼,要提高視窗響應速度,在“UI執行緒”中做的事自然是越少越好。如果某些API是可以跨執行緒使用的,在別的執行緒把該算的全算好,該IO的全做好,最後直接操作HWND,是最理想的狀況。而不是在操作HWND的前面一段時間就轉入“UI執行緒”。

 

APIIsGUIThread

非常有意思的事情,當筆者快寫完上面的文字的時候,卻發現了“IsGUIThread”這個函式。對此,本文當然有必要把這個函式中的GUI執行緒的概念考究清楚了。

 

調查發現,GUI執行緒是Windows核心中的概念。筆者對此並不無實際開發體驗,且摘錄一段查到的文字:

 

普通的Win32執行緒有兩個棧:一個是使用者棧,另一個是核心棧;而如果是核心中建立的系統工作執行緒,則只有核心棧。只要程式碼在核心中執行,執行緒就一定是使用其核心棧的。棧的主要作用是維護函式呼叫幀,以及為區域性變數提供空間。

 

使用者棧可以指定其大小,預設是1MB,通過編譯指令/stack可改設其他值。

 

普通核心棧的大小是固定的,由系統根據CPU架構而定,x86系統上為12KBx64系統上為24KB,安騰系統上為32KB。對於GUI執行緒,普通核心棧空間可能不夠,所以系統又定義了“大核心棧”概念,可以在需要的時候增長棧空間。只有GUI執行緒才能使用大核心棧,這也是系統規定的。

 

關於GUI執行緒,筆者多說幾句。Windows的發明,將GDIUSER模組,即“視窗與圖形模組”的實現移到了核心中,稱為Windows子系統核心服務,並形成一個win32k.sys核心檔案。而使用者層僅留呼叫介面,由User32.dllGDI32.dll兩個檔案暴露出來。判斷一個執行緒是不是GUI執行緒的依據,竟非常的簡單:執行緒初建時,都是普通執行緒,第一次呼叫Windows子系統核心服務(只要使用者程式呼叫了User32.dllGDI32.dll中的函式,並導致相關核心服務在核心中被執行),系統即立刻將之轉變為GUI執行緒,並從而切換到“大核心棧”;倘若至執行緒結束,並未有任何一個子系統核心服務被呼叫,那麼它一直都是普通執行緒,一直使用普通核心棧。

 

Windows核心中的核心棧(摘自《竹林蹊徑:深入淺出Windows驅動開發》)

http://yvqvan.blog.163.com/blog/static/254151032011321113127651/

 

從這段文字看,Windows核心的GUI執行緒概念和我們剛才所談的“UI執行緒”完全是兩個概念。因此,除了“UI執行緒不是官方概念”有待商榷以外,上文仍然成立。當然,官方概念叫“GUI執行緒”,還是說準確點為好。

 

總結

好了,下面簡單概括一下我要表達的觀點:

l  向無知者兜售自己創造的概念並試圖讓他奉為真理,是不厚道的。

l  MFC的文件中確實有“UI執行緒”的提法,它與工作執行緒分別由兩個不同版本的_AfxBeginThread建立,主要區別是“UI執行緒”具備一個訊息迴圈。不過我們可以覆蓋CWinThread::Run使得這個區別不存在。

l  Win32使用者態中,並不存在官方的“UI執行緒”概念。

l  並不是說所有操作HWND的函式都必須在建立HWND的執行緒中使用,事實上可能正好相反,或許只有少數函式有此限制。

l  Window核心中確實有GUI執行緒和工作執行緒的區分,但這與我們之前所要表達的“UI執行緒”並不是一個意思。

 

以上,懇請各位批評指正。如果你看到了這裡,那非常感謝你能看完。:) 

相關文章