1. 程式人生 > >Win32多執行緒程式設計學習心得

Win32多執行緒程式設計學習心得

此處部落格不再更新。

為什麼多執行緒?

多執行緒並不一定是最好的,合適才是最好的。

多執行緒主要的優點是價廉物美,啟動快、退出快、與其他執行緒共享核心物件,很容易實現共產主義的偉大夢想。但是其又有不可預期、測試困難的缺點。

使用好多執行緒,就是要知道何時應該用多執行緒,何時不該用。如果應該用多執行緒,如何解決Race Condition問題?如何共享資料?如何提高效率?如何同步執行緒和資料?總結起來就是:

  • 有始有終,執行緒的建立和釋放都要靠自己
  • 不拋棄不放棄,等一等執行緒,讓它做完自己的工作
  • 文明有序,資源佔用無衝突

但是有時候卻不建議使用多執行緒:

  • 針對於慢速I/O裝置,Overlapped I/O更能勝任
  • 程式的健壯性要求很高,值得付出比較多的額外負擔,多程序可能更能勝任

操作執行緒

如何建立執行緒?

如果要寫一個多執行緒程式,第一步就是建立一個執行緒,我們可以使用CreateThread API函式,也可以使用_beginthreadex C 函式,其實我大多數時候使用的是Boost庫上面的boost::thread物件來建立執行緒物件。如果有興趣可以看看Boost庫,這裡暫且不討論Boost庫thread。

如果使用上面兩個函式,可以去msdn檢視。使用上面兩種函式建立執行緒,其執行緒函式都必須符合以下格式,當然函式名可以更換:

DWORD WINAPI ThreadFunc(LPVOID
n)
;

使用CreateThread API函式或者_beginthreadex函式,可以傳回兩個值用以識別一個新的執行緒——返回值Handle(控制代碼)和輸出引數lpThread(執行緒ID)。為了安全防護的緣故,不能根據一個執行緒的ID獲得其handle。

如何釋放執行緒?

執行緒和程序一樣,都是核心物件。如何釋放執行緒屬於如何釋放核心物件的問題。CloseHandle函式在這裡起了十分重要的作用。CloseHandle函式的功能是將核心物件的引用計數減1。其不能直接用來釋放核心物件,核心物件只有在其引用計數為0的時候會被作業系統自動銷燬。

BOOL CloseHandle(HANDLE
hObject)
;

如果你不呼叫該函式,即使執行緒在建立之後執行完畢,引用計數還是不為0,執行緒無法被銷燬。如果一個程序沒有在結束之前對它所開啟的核心物件呼叫CloseHandle,作業系統會自動把那些物件的引用計數減一。雖然作業系統會做這個工作,但是他不知道核心物件實際的意義,也就不可能知道解構順序是否重要。如果你在迴圈結構建立了核心物件而沒有CloseHandle,好吧!你可能會有幾十萬個控制代碼沒有關閉,你的系統會因此沒有可用控制代碼,然後各種異常現象就出現了。記住當你完成你的工作,應該呼叫CloseHandle函式釋放核心物件。

在清理執行緒產生的核心物件時也要注意這個問題。不要依賴因執行緒結束而清理所有被這一執行緒產生的核心物件。面對一個開啟的物件,區分其擁有者是程序或是執行緒是很重要的。這決定了系統何時做清理工作。程式設計師不能選擇有程序或者執行緒擁有物件,一切都得視物件型別而定。如果被執行緒開啟的核心物件被程序擁有,執行緒結束是無法清理這些核心物件的。

執行緒核心物件與執行緒

其實這兩個是不同的概念。CreateThread函式返回的控制代碼其實是指向執行緒核心物件,而不是直接指向執行緒本身。在建立一個新的執行緒時,執行緒本身會開啟執行緒核心物件,引用計數加1,CreateThread函式返回一個執行緒核心物件控制代碼,引用計數再加1,所以執行緒核心物件一開始引用計數就是2。

呼叫CloseHandle函式,該執行緒核心物件引用計數減一,執行緒執行完成之後,引用計數再減一為零,該核心物件被自動銷燬。

結束主執行緒

首先得了解哪個執行緒是主執行緒:程式啟動後就執行的執行緒。主執行緒有兩個特點:

  • 負責GUI主訊息迴圈
  • 主執行緒結束時,強迫其他所有執行緒被迫結束,其他執行緒沒有機會執行清理工作

第二個特點也就意味著,如果你不等待其他執行緒結束,它們沒有機會執行完自己的操作,也沒有機會做最後的cleanup操作。我遇到過由於沒有等待,而出現程式奔潰的情況。反正很危險。

結束執行緒並獲取其結束程式碼

這個沒什麼好說的,可以使用ExitThread函式退出執行緒,返回一個結束程式碼。GetExitCodeThread函式獲取ExitThread函式或者return語句返回的結束程式碼。不過想通過GetExitCodeThread來等待執行緒結束是個很糟糕的注意——CPU被浪費了。下一節提及的WaitForSingleObject才是正道。

終止其他執行緒

終止其他執行緒可以使用TerminateThread()函式,也可以使用全域性標記。

TerminateThread()函式的缺點是:
1、執行緒沒有機會在結束前清理自己,其堆疊也沒有被釋放掉,出現記憶體洩露;
2、任何與此執行緒有附著關係的DLLs也沒有機會獲得執行緒解除附著通知;
3、執行緒進入的Critical Section將永遠處於鎖定狀態(Mutex會返回wait_abandoned狀態)。
4、執行緒正在處理的資料會處於不穩定狀態。

TerminateThread()唯一可以預期的是:執行緒handle變成激發狀態,並且傳回dwExitCode所指定的結束程式碼。

設立全域性標記的優點:保證目標執行緒在結束之前安全而一致的狀態
設立全域性標記的缺點:執行緒需要一個polling機制,時時檢查標記值。(可以使用一個手動重置的event物件

等一等執行緒

等待一個執行緒的結束

使用WaitForSingleObject最顯而易見的好處是你終於可以把以下程式碼精簡成一句了。

for(;;)
{
  int rc;
  rc = GetExitCodeThread(hThread, &exitCode);
  if(!rc && exitCode != STILL_ACTIVE)
    break;
}
→ → → → → →
WaitForSingleObject(hThread, INFINITE);

其他好處就是:

  • busy loop浪費太多CPU時間
  • 可以設定等待時間

等待多個執行緒的結束

WaitForSingleObject函式不好同時判斷多個執行緒的狀態,WaitForMultipleObjects可以同時等待多個執行緒,可以設定是否等待所有執行緒執行結束還是隻要一個執行緒執行完立馬返回。

在GUI執行緒中等待

在GUI執行緒中總是要常常回到主訊息迴圈,上述兩個wait api函式會阻塞主訊息迴圈。MsgWaitForMultipleObjects函式可以在物件唄激發或者訊息到達時被喚醒而返回。

執行緒同步

執行緒同步主要有Critical Sections、Mutex、Semaphores、Event,除了Critical Section是存在於程序記憶體空間內,其他都是核心物件

Critical Sections

Critical Section用來實現排他性佔有,適用範圍時單一程序的各個執行緒之間。

使用示例:

CRITICAL_SECTION cs ; // here must be global attributes to related thread
InitializeCriticalSection (&cs );
EnterCriticalSection(&cs );
LeaveCriticalSection(&cs );
DeleteCriticalSection(&cs );

Critical Sections注意事項:

  • 一旦執行緒進入一個Critical Section,再呼叫LeaveCriticalSection函式之前,就能一直重複的進入該Critical Section。
  • 千萬不要在一個Critical section之中呼叫Sleep()或者任何Wait... API函式。
  • 如果進入Critical section的那個執行緒結束了或者當掉了,而沒有呼叫LeaveCriticalSection函式,系統就沒有辦法將該Critical Section清除。

Critical Section的優點:

  • 相對於Mutex來說,其速度很快。鎖住一個未被擁有的mutex要比鎖住一個未被擁有的critical section,需要花費幾乎100倍時間。(critical section不需要進入作業系統核心)

Critical Section的缺陷:

  • Critical Section不是核心物件,無法WaitForSingleObject,沒有辦法解決死鎖問題(一個著名的死鎖問題:哲學家進餐問題)
  • Critical Section不可跨程序
  • 無法指定等待結束的時間長度
  • 不能夠同時有一個Critical section被等待
  • 無法偵測是否已被某個執行緒放棄

Mutex

Mutex可以在不同的執行緒之間實現排他性戰友,甚至即使那些執行緒屬於不同程序。

使用示例:

HANDLE hMutex ; // global attributes
hMutex = CreateMutex (
        NULL, // default event attributes
        false, // default not initially owned
        NULL // unnamed
       );
DWORD dwWaitResult = WaitForSingleObject (hMutex , INFINITE );
if (dwWaitResult == WAIT_OBJECT_0 )
{
        // wait succeed, do what you want
       ...
}
ReleaseMutex(hMutex );

示例解釋:
1、HMutex在建立時為未被擁有未激發狀態;
2、呼叫Wait...()函式,執行緒獲得hMutex的擁有權,HMutex短暫變成激發狀態,然後Wait...()函式返回,此時HMutex的狀態是被擁有未激發
3、ReleaseMutex之後,HMutex的狀態變為未被擁有未激發狀態

Mutex注意事項:

  • Mutex的擁有權並非屬於哪個產生它的哪個執行緒,而是那個最後對此mutex進行Wait...()操作並且尚未進行ReleaseMutex()操作的執行緒。
  • 如果執行緒擁有一個mutex而在結束前沒有呼叫ReleaseMutex(),mutex不會被摧毀,取而代之,該mutex會被視為“未被擁有”以及“未被激發”,而下一個等待中的執行緒會被以WAIT_ABANDONED_0通知。
  • Wait...()函式在Mutex處於未被擁有未被激發狀態時返回。
  • 將CreateMutex的第二個引數設為true,可以阻止race condition,否則呼叫CreateMutex的執行緒還未擁有Mutex,發生了context switch,就被別的執行緒擁有了。

Mutex優點

  • 核心物件,可以呼叫Wait...() API函式
  • 跨執行緒、跨程序、跨使用者(將CreateMutex的第三個引數前加上"Global//")
  • 可以具名,可以被其他程序開啟
  • 只能被擁有它的哪個執行緒釋放

Mutex缺點

  • 等待代價比較大

Semaphores

Semaphore被用來追蹤有限的資源。

和Mutex的對比

  • mutex是semaphore的退化,令semahpore的最大值為1,那就是一個mutex
  • semaphore沒有擁有權的概念,也沒有wait_abandoned狀態,一個執行緒可以反覆呼叫Wait...()函式以產生鎖定,而擁有mutex的執行緒不論在呼叫多少次Wait...()函式也不會被阻塞。
  • 在許多系統中都有semaphore的概念,而mutex則不一定。
  • 呼叫ReleaseSemaphore()的那個執行緒並不一定是呼叫Wait...()的那個執行緒,任何執行緒都可以在任何時間呼叫ReleaseSemaphore,解除被任何執行緒鎖定的Semaphore。

Semaphore優點

  • 核心物件
  • 可以具名,可以被其他程序開啟
  • 可以被任何一個執行緒釋放

Semaphore缺點

Event

Event通常用於overlapped I/O,或者用來設計某些自定義的同步物件。

使用示例:

HANDLE hEvent ; // global attributes
hEvent = CreateEvent (
        NULL, // default event attributes
        true, // mannual reset
        false, // nonsignaled
        NULL // unnamed
       );

SetEvent(hEvent);
PulseEvent(hEvent);
DWORD dwWaitResult = WaitForSingleObject (hEvent , INFINITE );
ResetEvent(hEvent);
if (dwWaitResult == WAIT_OBJECT_0 )
{
        // wait succeed, do what you want
       ...
        ResetEvent(hEvent );
}

示例解釋:
1、CreateEvent預設為非激發狀態、手動重置
2、SetEvent把hEvent設為激發狀態
3、在手動重置情況下(bManualReset=true),PulseEvent把event物件設為激發狀態,然而喚醒所有等待中的執行緒,然後恢復為非激發狀態;
4、在自動重置情況下(bManualReset=false),PulseEvent把event物件設為激發狀態,然而喚醒一個等待中的執行緒,然後恢復為非激發狀態;
5、ResetEvent將hEvent設為未激發狀態

Event注意事項:

  • CreateEvent函式的第二個引數bManualReset若為false,event會在變成激發狀態(因而喚醒一個執行緒)之後,自動重置為非激發狀態;
  • CreateEvent函式的第二個引數bManualReset若為true,event會在變成激發狀態(因而喚醒一個執行緒)之後,不會自動重置為非激發狀態,必須要手動ResetEvent;

Event優點:

  • 核心物件
  • 其狀態完全由程式來控制,其狀態不會因為Wait...()函式的呼叫而改變。
  • 適用於設計新的同步物件
  • 可以具名,可以被其他程序開啟

Event缺點:

  • 要求甦醒的請求並不會被儲存起來,可能會遺失掉。如果一個AutoReset event物件呼叫SetEvent或PulseEvent,而此時並沒有執行緒在等待,這個event會被遺失。如Wait...()函式還沒來得及呼叫就發生了Context Switch,這個時候SetEvent,這個要求甦醒的請求會被遺失,然後呼叫Wait...()函式執行緒卡死。

替代多執行緒

Overlapped I/O

Win32之中三個基本的I/O函式:CreateFile()、ReadFile()和WriteFile()。

  • 設定CreateFile()函式的dwFlagsAndAttributes引數為FILE_FLAG_OVERLAPPED,那麼對檔案的每一個操作都將是Overlapped。此時可以同時讀寫檔案的許多部分,沒有目前的檔案位置的概念,每一次讀寫都要包含其檔案位置。
  • 如果發出許多Overlapped請求,那麼執行順序無法保證。
  • Overlapped I/O不能使用C Runtime Library中的stdio.h函式,只能使用ReadFile()和WriteFile()來執行I/O。

Overlapped I/O函式使用OVERLAPPED結構來識別每一個目前正在進行的Overlapped操作,同時在程式和作業系統之間提供了一個共享區域,引數可以在該區域雙向傳遞。

多程序

如果一個程序死亡,系統中的其他程序還是可以繼續執行。多程序程式的健壯性遠勝於多執行緒。因為如果多個執行緒在同一個程序中執行,那麼一個誤入歧途的執行緒就可能把整個程序給毀了。

另一個使用多重程序的理由是,當一個程式從一個作業平臺被移植到另一個作業平臺,譬如Unix(不支援執行緒,但程序的產生與結束的代價並不昂貴),Unix應用程式往往使用多個程序,如果移植成為多執行緒模式,可能需要大改。

文獻