緣起
這是一篇找噴的文章。
由於一些歷史原因和人際淵源,周圍同事談論一些技術話題的時候,經常使用“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
MFC的AfxBeginThread提供了兩個版本:
| 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 } |
林林總總地貼了這么些代碼,差不多可以看出MFC的CWinThread的一些實現機制了。總的來說,MFC提供的“UI線程”,默認為線程實現了一個帶OnIdle機制的消息循環,同時,它Attach了應用程序主窗口,m_pMainWindow被設為了應用程序主窗口,它在OnIdle以及ProcessMessageFilter中被用到。
注意到在_AfxThreadEntry中有一行AfxInitThread,這里面注冊了一個消息鈎子,鈎子回調函數里面會調用ProcessMessageFilter。當處於幫助模式的時候,這個函數會向m_pMainWindow發送code為ID_HELP的WM_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. 不要在UI線程中做長時間的操作
- 2. 只能在UI線程中操作HWND
- 3. 我們搞個雙UI線程
第三點暫時無視吧,個人覺得無意義,什么單UI線程、雙UI線程,這個在系統層面根本沒這個提法以及限制,完全取決於開發者。倒是前兩者,是有那么一點意義的提法。為了描述方便,在本節中,暫且定義“UI線程”為,具有消息循環,並且在其中至少創建了一個可見窗口的線程。(這里可能有人會問,沒有窗口,你需要消息循環干嘛?一個例子:帶TimerProc的Timer需要消息循環。)
對於第一條,其實我並無多大異議,只要不去拷問別人什么叫“UI線程”,這樣輕描淡寫的提及,大家總是心知肚明的。第二條是創造“UI線程”概念的一個的很大的使用場景,然而這個命題本身卻是錯誤的。起碼,SendMessage和PostMessage是無論在哪個線程執行都是可行的,那么,那一大堆由GUI庫包裝SendMessage而成的窗口操作函數自然也是隨處可用的,以及本質上由SendMessage實現的一些窗口API也是隨處可用的。為了說清楚此問題,好像要總結一下哪些API需要在UI線程中執行,以及哪些不需要——但這是不可能的,下一節我略舉幾個例子。
本節結束之前,我想將上述1、2改個提法,以避免提及“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 SendMessage、PostMessage
這兩個當然不必在創建窗口的線程使用了。
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線程”。
API:IsGUIThread
非常有意思的事情,當筆者快寫完上面的文字的時候,卻發現了“IsGUIThread”這個函數。對此,本文當然有必要把這個函數中的GUI線程的概念考究清楚了。
調查發現,GUI線程是Windows內核中的概念。筆者對此並不無實際開發體驗,且摘錄一段查到的文字:
普通的Win32線程有兩個棧:一個是用戶棧,另一個是內核棧;而如果是內核中創建的系統工作線程,則只有內核棧。只要代碼在內核中運行,線程就一定是使用其內核棧的。棧的主要作用是維護函數調用幀,以及為局部變量提供空間。
用戶棧可以指定其大小,默認是1MB,通過編譯指令/stack可改設其他值。
普通內核棧的大小是固定的,由系統根據CPU架構而定,x86系統上為12KB,x64系統上為24KB,安騰系統上為32KB。對於GUI線程,普通內核棧空間可能不夠,所以系統又定義了“大內核棧”概念,可以在需要的時候增長棧空間。只有GUI線程才能使用大內核棧,這也是系統規定的。
關於GUI線程,筆者多說幾句。Windows的發明,將GDI和USER模塊,即“窗口與圖形模塊”的實現移到了內核中,稱為Windows子系統內核服務,並形成一個win32k.sys內核文件。而用戶層僅留調用接口,由User32.dll和GDI32.dll兩個文件暴露出來。判斷一個線程是不是GUI線程的依據,竟非常的簡單:線程初建時,都是普通線程,第一次調用Windows子系統內核服務(只要用戶程序調用了User32.dll和GDI32.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線程”並不是一個意思。
以上,懇請各位批評指正。如果你看到了這里,那非常感謝你能看完。:)
