1 為什么使用多線程
耗時的操作使用線程,提高應用程序響應(對圖形界面的程序尤為重要,多線程保證界面不卡,仍然可以響應鍵鼠)
並行操作使用線程,比如服務器響應客戶的請求。
多CPU或者多核系統中,多線程提高CPU利用率(OS保證線程數不大於CPU數目時,不同的線程在不同的CPU上)
改善程序結構。
2 線程的優點
與進程相比,它是一種花銷小,切換快,更節儉的多任務的操作方式。啟動一個新的進程必須分配獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,是一種昂貴的多任務工作方式;而如果在一個進程中用多線程,彼此之間使用相同的地址空間,共享數據,線程切換的代價很小。
另外是線程之間通信機制。不同的進程有獨立的數據空間,要進行數據傳遞只能通過通信的方式,費時又不方便。由於同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其他線程所用,方便。當然,數據在線程之間共享也帶來一些問題,所以需要同步。
3 理解線程
進程:每個進程由私有的虛擬地址空間、代碼、數據和其他系統資源組成。進程在運行時創建的資源隨着進程的終止而死亡。
因為進程有獨立的地址空間,一個進程崩潰后,在保護模式下,不會對其他進程產生影響,多進程程序比多線程健壯。
線程:獨立的執行流。缺省的運行包含一個主線恆,主線程以函數地址的形式,如main()或WinMain函數,提供程序的啟動點,主線程終止時,進程也隨之終止。
一個進程中的所有線程都在該進程的虛擬空間中,使用該進程的全局變量和系統資源。CPU時間片輪轉的方式調度,優先級高的先運行。
4 線程分類
用戶界面線程:通常用來處理用戶輸入並響應各種事件和消息,其實,應用程序的主執行線程CWinAPP對象就是一個用戶界面線程,當應用程序啟動時自動創建和啟動,同樣它的終止也意味着程序結束,進程終止。
工作線程(后台線程):執行程序的后台處理任務,比如計算、調度、對串口的讀寫操作等,和用戶界面的區別是,不用從CWinThread類派生來創建。工作線程和用戶界面線程啟動時要調用同一個函數的不同版本。
5 線程
5.1 線程組成:線程由線程ID、當前指令指針(PC),寄存器集合和堆棧組成。
5.2 線程狀態:就緒(等待處理機),阻塞(等待事件),運行。
6 線程同步互斥的4種方式
線程之間同步分兩大類:用戶模式和內核模式。內核模式就是利用內核對象的單一性來進行同步,使用的時需要切換內核態與用戶態,而用戶模式就是不需要切換到內核態,只在用戶態完成操作。
用戶模式: 臨界區;
內核模式: 事件, 信號量,互斥量。
6.1 臨界區(Cirtical Section) :適合一個進程內多個線程訪問公共區域或代碼段時使用。
InitializeCriticalSection , DeleteCriticalSection, EnterCriticalSection, LeaveCriticalSection
優點:效率高,不是內核對象,涉及到內核態和用戶態的切換。
缺點:不是內核對象,無法獲知進入臨界區的線程是生還是死,如果進入臨界區的線程掛了,沒有釋放資源,系統無法獲知,而且沒辦法釋放。
注意:EnterCriticalSection,一個線程可以多次進入關鍵區域
先找到關鍵段CRITICAL_SECTION的定義吧,它在WinBase.h中被定義成RTL_CRITICAL_SECTION。而RTL_CRITICAL_SECTION在WinNT.h中聲明,它其實是個結構體:
typedefstruct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // 調試相關的
LONG LockCount; // n表示有n個線程在等待
LONGRecursionCount; //擁有該關鍵段的線程對此資源獲得的關鍵段次數
HANDLEOwningThread; // 擁有該關鍵段的線程句柄,from the thread's ClientId->UniqueThread
HANDLE LockSemaphore; // 一個自復位事件
DWORD SpinCount; // 旋轉鎖的設置,單CPU下忽略
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
由這個結構可以知道關鍵段會記錄擁有該關鍵段的線程句柄即關鍵段是有“線程所有權”概念的。第四個參數OwningThread記錄獲准進入關鍵區域的線程句柄,如果這個線程再次進入,EnterCiritcalSection()會更新第三個參數RecursionCount,來記錄該線程進入的次數並立即返回讓該線程進入。一旦擁有線程所有權的線程調用LeaveCriticalSection()使其進入的次數為0時,系統會自動更新關鍵段並將等待中的線程換回可調度狀態。
注意:擁有線程所有權的線程可以重復進入關鍵代碼區域。
另外,由於將線程切換到等待狀態的開銷較大,因此為了提高關鍵段的性能,Microsoft將旋轉鎖合並到關鍵段中,這樣EnterCriticalSection()會先用一個旋轉鎖不斷循環,嘗試一段時間才會將線程切換到等待狀態。《Windows核心編程》第五版的第八章推薦在使用關鍵段的時候同時使用旋轉鎖,這樣有助於提高性能。值得注意的是如果主機只有一個處理器,那么設置旋轉鎖是無效的。無法進入關鍵區域的線程總會被系統將其切換到等待狀態。
下面是配合了旋轉鎖的關鍵段初始化函數
函數功能:初始化關鍵段並設置旋轉次數
函數原型:
BOOLInitializeCriticalSectionAndSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
函數說明:旋轉次數一般設置為4000。
函數功能:修改關鍵段的旋轉次數
函數原型:
DWORDSetCriticalSectionSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
6.2 互斥量(Mutex):適合不同進程內多線程訪問公共區域或代碼段時使用,與臨界區相似。
內核態,可以跨進程同步,雖然比臨界區低效率,但是不會像臨界區那樣死等,waitforsingleobject可以設定TIMEOUT時間。
如果一個擁有Mutex的線程在返回之前沒有調用ReleaseMutex(),那么這個mutex就被舍棄了,但是當其他線程等待這個Mutex
時,仍能返回,並得到一個WAIT_ABANDONED_0返回值。 能夠找到Mutex被舍棄是MUTEXTC特有的。
CreateMutex , CloseHandle, WaitForSingleObject, ReleaseMutex.
互斥量也是一個內核對象,它用來確保一個線程獨占一個資源的訪問。互斥量與關鍵段的行為非常相似,並且互斥量可以用於不同進程中的線程互斥訪問資源。使用互斥量Mutex主要將用到四個函數。
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes, //表示安全控制,一般直接傳入NULL
BOOLbInitialOwner, // 是否讓創建者(此例中是主線程)擁有該互斥對象,和臨界區一樣,也有所有權的問題
// 如果是ture, 表示當前創建線程擁有這個mutex;
// 如果是false,表示第一個調用waitforsngleobject的線程會擁有這個mutex.
LPCTSTRlpName,
);
注意:Mutex也有線程所有權的問題,擁有這個Mutex的線程調用waitforsngleobject,會得到這個mutex,不會阻塞。
6.3 事件(Event):通過線程間觸發事件實現同步互斥。
CreateEvent, CloseHandle, SetEvent, ResetEvent
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 表示安全控制,一般直接傳入NULL。
BOOLb ManualReset, // 手動還是自動復位。
BOOL bInitialState, // 初始狀態是否是已經觸發的。
LPCTSTR lpName // 事件的名稱,NULL表示匿名事件。
);
6.4 信號量(Semaphore):與臨界區和互斥量不同,可以實現多個線程同時訪問公共區域數據,原理與OS中的PV操作類似,先設置一個訪問公共區域的線程最大連接數,每有一個線程訪問共享數就減一,直到資源小與等於0.
CreateSemaphore, CLoseHandle, WaitForSingleObject, ReleaseSemaphore
注意:Semaphore沒有所有權的觀念,一個線程可以反復調用wait...()函數以產生新的鎖定。這和mutex絕不相同:擁有mutex的線程不論再調用多少次wait...()函數,也不會被阻塞住。
6 線程解析
Windows提供了兩種線程,輔助線程和用戶界面線程。兩種線程均為MFC庫所支持。用戶界面線程通常有窗口,因此,它具有自己的消息循環。輔助線程沒有窗口,因此,它不需要處理消息。輔助線程比較易於編程,而且通常更加有用
每個線程都有自己的專有寄存器(棧指針、程序計數器),但代碼區是共享的.
程序計數器:程序計數器是用於存放下一條指令所在單元的地址的地方。
7 創建線程的三種方法
CreateThread() : WIndows API
_beginthread():CRT
_beginthreadEx(): 比 _beginthread多幾個參數
AfxBeginThread() : MFC
8 退出線程的方法函數
return: 這個是最安全的方法。return后,會析構線程函數內的對象,自動調用_endthreadex()清理_beginthreadex() 申請的資源。
調用_endthreadex()函數或_ExitThread()函數(最好不用,不會調用線程函數內申請的類的對象的析構函數,會導致內存泄漏)
調用TerminateThread(必須避免)
9 線程的掛起,喚醒,和終止
SuspendThread: 掛起
RessumeThread: 喚醒
TerminateThread: 停止
10 進程 VS 線程
A:進程是資源分配的基本單位,線程是CPU調度的的最小單位。
B: 進程有獨立的地址空間,建立數據表來維護代碼段,堆棧段和數據段。而一個進程中的多個線程共享大部分數據,使用相同的地址空間,線程之間切換
快,當然,線程有自己的局部變量和棧,
C:線程之間通信方便,比如一些共享的數據(全局變量,靜態變量),線程之間
要通過同步和互斥。 而進程之間通信通過 進程通信的方式進行。
D:進程比線程健壯,線程死掉,整個進程就死掉;進程對其他進程沒有影響。
E: 進程有個PCB表,線程有自己的TCB。但是TCB的信息比PCB要少很多。
11 進程之間通信方式
Socket
COM/DCOM
共享內存
管道:匿名管道
命名管道
郵件槽
信號量
12 線程函數
DWORD WINAPI 函數名(LPVOID lpParam)
13 互斥量與臨界區 區別
A:互斥量是內核對象,比臨界區耗費資源,但是可以命名,可以被其他進程訪問
B::臨界區是通過對多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據
訪問。
互斥量是為協調共同對一個共享資源的單獨訪問而設計。
C:臨界區是表示一段代碼不能同時執行;mutext可以用來同步兩段代碼。
14 生產者 消費者
兩個信號量:一個empty,初始值N(N是緩沖區大小);另外一個是full,初始值哦;
一個Mutex: 生成者之間,消費者之間,生產者和消費者之間都需要互斥。
15:讀寫鎖:同時可以有多個讀,但是只能被一個寫者擁有。
readcount - 記錄讀的個數,需要互斥訪問。
READ:
Repeat;
P(mutex); // 對readcount互斥訪問
readcount:=readcount+1; // 讀線程個數加1
if(readcount=1) // 如果是第一個讀線程,就請求讀/寫鎖
P(write);
V(mutex); // 對readcount操作完畢
讀文件 操作
P(mutex); // 讀完,又要操作readcount
readcount:=readcount-1;
if(readcount=0) // 如果是最后一個讀線程,也要釋放讀/寫鎖
V(write);
V(mutex); // 操作完readcount
Until false
WRITE:
Repeat
P(write);
寫文件操作
V(write);
Until false;
上述方法有可可能造成讀一直進行,但是寫沒有機會執行:可以在讀的時候,如果發現有寫操作在等待,讀操作就等待