Win32多線程程序設計讀書筆記

為什么多線程?


多線程并不一定是最好的,合適才是最好的。

多線程主要的優(yōu)點是價廉物美,啟動快、退出快、與其他線程共享核心對象,很容易實現(xiàn)共產(chǎn)主義的偉大夢想。但是其又有不可預期、測試困難的缺點。

使用好多線程,就是要知道何時應該用多線程,何時不該用。如果應該用多線程,如何解決Race Condition問題?如何共享數(shù)據(jù)?如何提高效率?如何同步線程和數(shù)據(jù)?總結起來就是:

  • 有始有終,線程的創(chuàng)建和釋放都要靠自己
  • 不拋棄不放棄,等一等線程,讓它做完自己的工作
  • 文明有序,資源占用無沖突

但是有時候卻不建議使用多線程:

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

操作線程


如何創(chuàng)建線程?

如果要寫一個多線程程序,第一步就是創(chuàng)建一個線程,我們可以使用CreateThread API函數(shù),也可以使用_beginthreadex C 函數(shù),其實我大多數(shù)時候使用的是Boost庫上面的boost::thread對象來創(chuàng)建線程對象。如果有興趣可以看看Boost庫,這里暫且不討論Boost庫thread。

如果使用上面兩個函數(shù),可以去msdn查看。使用上面兩種函數(shù)創(chuàng)建線程,其線程函數(shù)都必須符合以下格式,當然函數(shù)名可以更換:

DWORD WINAPI ThreadFunc(LPVOID n);

使用CreateThread API函數(shù)或者_beginthreadex函數(shù),可以傳回兩個值用以識別一個新的線程——返回值Handle(句柄)和輸出參數(shù)lpThread(線程ID)。為了安全防護的緣故,不能根據(jù)一個線程的ID獲得其handle。

如何釋放線程?

線程和進程一樣,都是核心對象。如何釋放線程屬于如何釋放核心對象的問題。CloseHandle函數(shù)在這里起了十分重要的作用。CloseHandle函數(shù)的功能是將核心對象的引用計數(shù)減1。其不能直接用來釋放核心對象,核心對象只有在其引用計數(shù)為0的時候會被操作系統(tǒng)自動銷毀。

BOOL CloseHandle(HANDLE hObject);

如果你不調(diào)用該函數(shù),即使線程在創(chuàng)建之后執(zhí)行完畢,引用計數(shù)還是不為0,線程無法被銷毀。如果一個進程沒有在結束之前對它所打開的核心對象調(diào)用CloseHandle,操作系統(tǒng)會自動把那些對象的引用計數(shù)減一。雖然操作系統(tǒng)會做這個工作,但是他不知道核心對象實際的意義,也就不可能知道解構順序是否重要。如果你在循環(huán)結構創(chuàng)建了核心對象而沒有CloseHandle,好吧!你可能會有幾十萬個句柄沒有關閉,你的系統(tǒng)會因此沒有可用句柄,然后各種異?,F(xiàn)象就出現(xiàn)了。記住當你完成你的工作,應該調(diào)用CloseHandle函數(shù)釋放核心對象。

在清理線程產(chǎn)生的核心對象時也要注意這個問題。不要依賴因線程結束而清理所有被這一線程產(chǎn)生的核心對象。面對一個打開的對象,區(qū)分其擁有者是進程或是線程是很重要的。這決定了系統(tǒng)何時做清理工作。程序員不能選擇有進程或者線程擁有對象,一切都得視對象類型而定。如果被線程打開的核心對象被進程擁有,線程結束是無法清理這些核心對象的。

線程核心對象與線程

其實這兩個是不同的概念。CreateThread函數(shù)返回的句柄其實是指向線程核心對象,而不是直接指向線程本身。在創(chuàng)建一個新的線程時,線程本身會開啟線程核心對象,引用計數(shù)加1,CreateThread函數(shù)返回一個線程核心對象句柄,引用計數(shù)再加1,所以線程核心對象一開始引用計數(shù)就是2。

調(diào)用CloseHandle函數(shù),該線程核心對象引用計數(shù)減一,線程執(zhí)行完成之后,引用計數(shù)再減一為零,該核心對象被自動銷毀。

結束主線程

首先得了解哪個線程是主線程:程序啟動后就執(zhí)行的線程。主線程有兩個特點:

  • 負責GUI主消息循環(huán)
  • 主線程結束時,強迫其他所有線程被迫結束,其他線程沒有機會執(zhí)行清理工作

第二個特點也就意味著,如果你不等待其他線程結束,它們沒有機會執(zhí)行完自己的操作,也沒有機會做最后的cleanup操作。我遇到過由于沒有等待,而出現(xiàn)程序奔潰的情況。反正很危險。

結束線程并獲取其結束代碼

這個沒什么好說的,可以使用ExitThread函數(shù)退出線程,返回一個結束代碼。GetExitCodeThread函數(shù)獲取ExitThread函數(shù)或者return語句返回的結束代碼。不過想通過GetExitCodeThread來等待線程結束是個很糟糕的注意——CPU被浪費了。下一節(jié)提及的WaitForSingleObject才是正道。

終止其他線程

終止其他線程可以使用TerminateThread()函數(shù),也可以使用全局標記。

TerminateThread()函數(shù)的缺點是:
1、線程沒有機會在結束前清理自己,其堆棧也沒有被釋放掉,出現(xiàn)內(nèi)存泄露;
2、任何與此線程有附著關系的DLLs也沒有機會獲得線程解除附著通知;
3、線程進入的Critical Section將永遠處于鎖定狀態(tài)(Mutex會返回wait_abandoned狀態(tài))。
4、線程正在處理的數(shù)據(jù)會處于不穩(wěn)定狀態(tài)。

TerminateThread()唯一可以預期的是:線程handle變成激發(fā)狀態(tài),并且傳回dwExitCode所指定的結束代碼。

設立全局標記的優(yōu)點:保證目標線程在結束之前安全而一致的狀態(tài)
設立全局標記的缺點:線程需要一個polling機制,時時檢查標記值。(可以使用一個手動重置的event對象

等一等線程


等待一個線程的結束

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

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

其他好處就是:

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

等待多個線程的結束

WaitForSingleObject函數(shù)不好同時判斷多個線程的狀態(tài),WaitForMultipleObjects可以同時等待多個線程,可以設定是否等待所有線程執(zhí)行結束還是只要一個線程執(zhí)行完立馬返回。

在GUI線程中等待

在GUI線程中總是要常?;氐街飨⒀h(huán),上述兩個wait api函數(shù)會阻塞主消息循環(huán)。MsgWaitForMultipleObjects函數(shù)可以在對象唄激發(fā)或者消息到達時被喚醒而返回。

線程同步


線程同步主要有Critical Sections、Mutex、Semaphores、Event,除了Critical Section是存在于進程內(nèi)存空間內(nèi),其他都是核心對象。

Critical Sections

Critical Section用來實現(xiàn)排他性占有,適用范圍時單一進程的各個線程之間。

使用示例:

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

Critical Sections注意事項:

  • 一旦線程進入一個Critical Section,再調(diào)用LeaveCriticalSection函數(shù)之前,就能一直重復的進入該Critical Section。
  • 千萬不要在一個Critical section之中調(diào)用Sleep()或者任何Wait... API函數(shù)。
  • 如果進入Critical section的那個線程結束了或者當?shù)袅耍鴽]有調(diào)用LeaveCriticalSection函數(shù),系統(tǒng)就沒有辦法將該Critical Section清除。

Critical Section的優(yōu)點:

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

Critical Section的缺陷:

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

Mutex

Mutex可以在不同的線程之間實現(xiàn)排他性戰(zhàn)友,甚至即使那些線程屬于不同進程。

使用示例:

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在創(chuàng)建時為未被擁有未激發(fā)狀態(tài);
2、調(diào)用Wait...()函數(shù),線程獲得hMutex的擁有權,HMutex短暫變成激發(fā)狀態(tài),然后Wait...()函數(shù)返回,此時HMutex的狀態(tài)是被擁有未激發(fā);
3、ReleaseMutex之后,HMutex的狀態(tài)變?yōu)?code>未被擁有和未激發(fā)狀態(tài)

Mutex注意事項:

  • Mutex的擁有權并非屬于哪個產(chǎn)生它的哪個線程,而是那個最后對此mutex進行Wait...()操作并且尚未進行ReleaseMutex()操作的線程。
  • 如果線程擁有一個mutex而在結束前沒有調(diào)用ReleaseMutex(),mutex不會被摧毀,取而代之,該mutex會被視為“未被擁有”以及“未被激發(fā)”,而下一個等待中的線程會被以WAIT_ABANDONED_0通知。
  • Wait...()函數(shù)在Mutex處于未被擁有未被激發(fā)狀態(tài)時返回。
  • 將CreateMutex的第二個參數(shù)設為true,可以阻止race condition,否則調(diào)用CreateMutex的線程還未擁有Mutex,發(fā)生了context switch,就被別的線程擁有了。

Mutex優(yōu)點

  • 核心對象,可以調(diào)用Wait...() API函數(shù)
  • 跨線程、跨進程、跨用戶(將CreateMutex的第三個參數(shù)前加上"Global//")
  • 可以具名,可以被其他進程開啟
  • 只能被擁有它的哪個線程釋放

Mutex缺點

  • 等待代價比較大

Semaphores

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

和Mutex的對比

  • mutex是semaphore的退化,令semahpore的最大值為1,那就是一個mutex
  • semaphore沒有擁有權的概念,也沒有wait_abandoned狀態(tài),一個線程可以反復調(diào)用Wait...()函數(shù)以產(chǎn)生鎖定,而擁有mutex的線程不論在調(diào)用多少次Wait...()函數(shù)也不會被阻塞。
  • 在許多系統(tǒng)中都有semaphore的概念,而mutex則不一定。
  • 調(diào)用ReleaseSemaphore()的那個線程并不一定是調(diào)用Wait...()的那個線程,任何線程都可以在任何時間調(diào)用ReleaseSemaphore,解除被任何線程鎖定的Semaphore。

Semaphore優(yōu)點

  • 核心對象
  • 可以具名,可以被其他進程開啟
  • 可以被任何一個線程釋放

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默認為非激發(fā)狀態(tài)、手動重置
2、SetEvent把hEvent設為激發(fā)狀態(tài)
3、在手動重置情況下(bManualReset=true),PulseEvent把event對象設為激發(fā)狀態(tài),然而喚醒所有等待中的線程,然后恢復為非激發(fā)狀態(tài);
4、在自動重置情況下(bManualReset=false),PulseEvent把event對象設為激發(fā)狀態(tài),然而喚醒一個等待中的線程,然后恢復為非激發(fā)狀態(tài);
5、ResetEvent將hEvent設為未激發(fā)狀態(tài)

Event注意事項:

  • CreateEvent函數(shù)的第二個參數(shù)bManualReset若為false,event會在變成激發(fā)狀態(tài)(因而喚醒一個線程)之后,自動重置為非激發(fā)狀態(tài);
  • CreateEvent函數(shù)的第二個參數(shù)bManualReset若為true,event會在變成激發(fā)狀態(tài)(因而喚醒一個線程)之后,不會自動重置為非激發(fā)狀態(tài),必須要手動ResetEvent;

Event優(yōu)點:

  • 核心對象
  • 其狀態(tài)完全由程序來控制,其狀態(tài)不會因為Wait...()函數(shù)的調(diào)用而改變。
  • 適用于設計新的同步對象
  • 可以具名,可以被其他進程開啟

Event缺點:

  • 要求蘇醒的請求并不會被存儲起來,可能會遺失掉。如果一個AutoReset event對象調(diào)用SetEvent或PulseEvent,而此時并沒有線程在等待,這個event會被遺失。如Wait...()函數(shù)還沒來得及調(diào)用就發(fā)生了Context Switch,這個時候SetEvent,這個要求蘇醒的請求會被遺失,然后調(diào)用Wait...()函數(shù)線程卡死。

替代多線程


Overlapped I/O

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

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

Overlapped I/O函數(shù)使用OVERLAPPED結構來識別每一個目前正在進行的Overlapped操作,同時在程序和操作系統(tǒng)之間提供了一個共享區(qū)域,參數(shù)可以在該區(qū)域雙向傳遞。

多進程

如果一個進程死亡,系統(tǒng)中的其他進程還是可以繼續(xù)執(zhí)行。多進程程序的健壯性遠勝于多線程。因為如果多個線程在同一個進程中運行,那么一個誤入歧途的線程就可能把整個進程給毀了。

另一個使用多重進程的理由是,當一個程序從一個作業(yè)平臺被移植到另一個作業(yè)平臺,譬如Unix(不支持線程,但進程的產(chǎn)生與結束的代價并不昂貴),Unix應用程序往往使用多個進程,如果移植成為多線程模式,可能需要大改。

文獻



歡迎訪問我的個人博客click me
博客原文地址:Win32 MultiThread Study Summary - Let's Thread

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容