《程式設計思想之多執行緒與多程式(1)——以作業系統的角度述說執行緒與程式》一文詳細講述了執行緒、程式的關係及在作業系統中的表現,《程式設計思想之多執行緒與多程式(2)——執行緒優先順序與執行緒安全》一文講了執行緒安全(各種同步鎖)和優先順序,這是多執行緒學習必須瞭解的基礎。本文將接著講一下C++中多執行緒程式的開發.這裡主要講Windows平臺執行緒的用法,建立執行緒要呼叫windows API的CreateThread方法。
建立執行緒
在Windows平臺,Windows API提供了對多執行緒的支援。前面程式和執行緒的概念中我們提到,一個程式至少有一個執行緒,這個執行緒稱為主執行緒(main thread),如果我們不顯示地建立執行緒,那我們產的程式就是隻有主執行緒的間執行緒程式。
下面,我們看看Windows中執行緒相關的操作和方法:
CreateThread與CloseHandle
CreateThread用於建立一個執行緒,其函式原型如下:
1 2 3 4 5 6 7 8 |
HANDLE WINAPI CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, //執行緒安全相關的屬性,常置為NULL SIZE_T dwStackSize, //新執行緒的初始化棧在大小,可設定為0 LPTHREAD_START_ROUTINE lpStartAddress, //被執行緒執行的回撥函式,也稱為執行緒函式 LPVOID lpParameter, //傳入執行緒函式的引數,不需傳遞引數時為NULL DWORD dwCreationFlags, //控制執行緒建立的標誌 LPDWORD lpThreadId //傳出引數,用於獲得執行緒ID,如果為NULL則不返回執行緒ID ); |
**說明:**lpThreadAttributes:指向SECURITY_ATTRIBUTES結構的指標,決定返回的控制程式碼是否可被子程式繼承,如果為NULL則表示返回的控制程式碼不能被子程式繼承。
dwStackSize :執行緒棧的初始化大小,位元組單位。系統分配這個值對
lpStartAddress:指向一個函式指標,該函式將被執行緒呼叫執行。因此該函式也被稱為執行緒函式(ThreadProc),是執行緒執行的起始地址,執行緒函式是一個回撥函式,由作業系統線上程中呼叫。執行緒函式的原型如下:
1 |
DWORD WINAPI ThreadProc(LPVOID lpParameter); //lpParameter是傳入的引數,是一個空指標 |
lpParameter:傳入執行緒函式(ThreadProc)的引數,不需傳遞引數時為NULL
dwCreationFlags:控制執行緒建立的標誌,有三個型別,0:執行緒建立後立即執行執行緒;CREATE_SUSPENDED:執行緒建立後進入就緒狀態,直到執行緒被喚醒時才呼叫;STACK_SIZE_PARAM_IS_A_RESERVATION:dwStackSize 引數指定執行緒初始化棧的大小,如果STACK_SIZE_PARAM_IS_A_RESERVATION標誌未指定,dwStackSize將會設為系統預留的值。
返回值:如果執行緒建立成功,則返回這個新執行緒的控制程式碼,否則返回NULL。如果執行緒建立失敗,可通過GetLastError函式獲得錯誤資訊。
1 |
BOOL WINAPI CloseHandle(HANDLE hObject); //關閉一個被開啟的物件控制程式碼 |
可用這個函式關閉建立的執行緒控制程式碼,如果函式執行成功則返回true(非0),如果失敗則返回false(0),如果執行失敗可呼叫GetLastError.函式獲得錯誤資訊。
【Demo1】:建立一個最簡單的執行緒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include "stdafx.h" #include <windows.h> #include <iostream> using namespace std; //執行緒函式 DWORD WINAPI ThreadProc(LPVOID lpParameter) { for (int i = 0; i < 5; ++ i) { cout << "子執行緒:i = " << i << endl; Sleep(100); } return 0L; } int main() { //建立一個執行緒 HANDLE thread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //關閉執行緒 CloseHandle(thread); //主執行緒的執行路徑 for (int i = 0; i < 5; ++ i) { cout << "主執行緒:i = " << i << endl; Sleep(100); } return 0; } |
結果如下:
1 2 3 4 5 6 7 8 9 10 |
主執行緒:i = 0 子執行緒:i = 0 主執行緒:i = 1 子執行緒:i = 1 子執行緒:i = 2 主執行緒:i = 2 子執行緒:i = 3 主執行緒:i = 3 子執行緒:i = 4 主執行緒:i = 4 |
【Demo2】:線上程函式中傳入引數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include "stdafx.h" #include <windows.h> #include <iostream> using namespace std; #define NAME_LINE 40 //定義執行緒函式傳入引數的結構體 typedef struct __THREAD_DATA { int nMaxNum; char strThreadName[NAME_LINE]; __THREAD_DATA() : nMaxNum(0) { memset(strThreadName, 0, NAME_LINE * sizeof(char)); } }THREAD_DATA; //執行緒函式 DWORD WINAPI ThreadProc(LPVOID lpParameter) { THREAD_DATA* pThreadData = (THREAD_DATA*)lpParameter; for (int i = 0; i < pThreadData->nMaxNum; ++ i) { cout << pThreadData->strThreadName << " --- " << i << endl; Sleep(100); } return 0L; } int main() { //初始化執行緒資料 THREAD_DATA threadData1, threadData2; threadData1.nMaxNum = 5; strcpy(threadData1.strThreadName, "執行緒1"); threadData2.nMaxNum = 10; strcpy(threadData2.strThreadName, "執行緒2"); //建立第一個子執行緒 HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc, &threadData1, 0, NULL); //建立第二個子執行緒 HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc, &threadData2, 0, NULL); //關閉執行緒 CloseHandle(hThread1); CloseHandle(hThread2); //主執行緒的執行路徑 for (int i = 0; i < 5; ++ i) { cout << "主執行緒 === " << i << endl; Sleep(100); } system("pause"); return 0; } |
結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
主執行緒 === 執行緒1 — 0 0 執行緒2 — 0 執行緒1 — 1 主執行緒 === 1 執行緒2 — 1 主執行緒 === 2 執行緒1 — 2 執行緒2 — 2 主執行緒 === 3 執行緒2 — 3 執行緒1 — 3 主執行緒 === 4 執行緒2 — 4 執行緒1 — 4 執行緒2 — 5 請按任意鍵繼續… 執行緒2 — 6 執行緒2 — 7 執行緒2 — 8 執行緒2 — 9 |
CreateMutex、WaitForSingleObject、ReleaseMutex
從【Demo2】中可以看出,雖然建立的子執行緒都正常執行起來了,但輸出的結果並不是我們預期的效果。我們預期的效果是每輸出一條語句後自動換行,但結果卻並非都是這樣。這是因為線上程執行時沒有做同步處理,比如第一行的輸出,主執行緒輸出“主執行緒 ===”後時間片已用完,這時輪到子執行緒1輸出,在子執行緒輸出“執行緒1 —”後時間片也用完了,這時又輪到主執行緒執行輸出“0”,之後又輪到子執行緒1輸出“0”。於是就出現了“主執行緒 === 執行緒1 — 0 0”的結果。
主執行緒:cout << “主執行緒 === ” << i << endl;
子執行緒:cout << pThreadData->strThreadName << ” — ” << i << endl;
為避免出現這種情況,我們對執行緒做一些簡單的同步處理,這裡我們用互斥量(Mutex),關於互斥量(Mutex)的概念,請看《程式設計思想之多執行緒與多程式(2)——執行緒優先順序與執行緒安全》一文;更多C++執行緒同步的處理,請看下一節。
在使用互斥量進行執行緒同步時會用到以下幾個函式:
1 2 3 4 5 |
HANDLE WINAPI CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, //執行緒安全相關的屬性,常置為NULL BOOL bInitialOwner, //建立Mutex時的當前執行緒是否擁有Mutex的所有權 LPCTSTR lpName //Mutex的名稱 ); |
**說明:**lpMutexAttributes也是表示安全的結構,與CreateThread中的lpThreadAttributes功能相同,表示決定返回的控制程式碼是否可被子程式繼承,如果為NULL則表示返回的控制程式碼不能被子程式繼承。bInitialOwner表示建立Mutex時的當前執行緒是否擁有Mutex的所有權,若為TRUE則指定為當前的建立執行緒為Mutex物件的所有者,其它執行緒訪問需要先ReleaseMutex。lpName為Mutex的名稱。
1 2 3 4 |
DWORD WINAPI WaitForSingleObject( HANDLE hHandle, //要獲取的鎖的控制程式碼 DWORD dwMilliseconds //超時間隔 ); |
**說明:**WaitForSingleObject的作用是等待一個指定的物件(如Mutex物件),直到該物件處於非佔用的狀態(如Mutex物件被釋放)或超出設定的時間間隔。除此之外,還有一個與它類似的函式WaitForMultipleObjects,它的作用是等待一個或所有指定的物件,直到所有的物件處於非佔用的狀態,或超出設定的時間間隔。
hHandle:要等待的指定物件的控制程式碼。dwMilliseconds:超時的間隔,以毫秒為單位;如果dwMilliseconds為非0,則等待直到dwMilliseconds時間間隔用完或物件變為非佔用的狀態,如果dwMilliseconds 為INFINITE則表示無限等待,直到等待的物件處於非佔用的狀態。
1 |
BOOL WINAPI ReleaseMutex(HANDLE hMutex); |
說明:釋放所擁有的互斥量鎖物件,hMutex為釋放的互斥量的控制程式碼。
【Demo3】:執行緒同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
#include "stdafx.h" #include <windows.h> #include <iostream> #define NAME_LINE 40 //定義執行緒函式傳入引數的結構體 typedef struct __THREAD_DATA { int nMaxNum; char strThreadName[NAME_LINE]; __THREAD_DATA() : nMaxNum(0) { memset(strThreadName, 0, NAME_LINE * sizeof(char)); } }THREAD_DATA; HANDLE g_hMutex = NULL; //互斥量 //執行緒函式 DWORD WINAPI ThreadProc(LPVOID lpParameter) { THREAD_DATA* pThreadData = (THREAD_DATA*)lpParameter; for (int i = 0; i < pThreadData->nMaxNum; ++ i) { //請求獲得一個互斥量鎖 WaitForSingleObject(g_hMutex, INFINITE); cout << pThreadData->strThreadName << " --- " << i << endl; Sleep(100); //釋放互斥量鎖 ReleaseMutex(g_hMutex); } return 0L; } int main() { //建立一個互斥量 g_hMutex = CreateMutex(NULL, FALSE, NULL); //初始化執行緒資料 THREAD_DATA threadData1, threadData2; threadData1.nMaxNum = 5; strcpy(threadData1.strThreadName, "執行緒1"); threadData2.nMaxNum = 10; strcpy(threadData2.strThreadName, "執行緒2"); //建立第一個子執行緒 HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc, &threadData1, 0, NULL); //建立第二個子執行緒 HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc, &threadData2, 0, NULL); //關閉執行緒 CloseHandle(hThread1); CloseHandle(hThread2); //主執行緒的執行路徑 for (int i = 0; i < 5; ++ i) { //請求獲得一個互斥量鎖 WaitForSingleObject(g_hMutex, INFINITE); cout << "主執行緒 === " << i << endl; Sleep(100); //釋放互斥量鎖 ReleaseMutex(g_hMutex); } system("pause"); return 0; } |
結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
主執行緒 === 0 執行緒1 — 0 執行緒2 — 0 主執行緒 === 1 執行緒1 — 1 執行緒2 — 1 主執行緒 === 2 執行緒1 — 2 執行緒2 — 2 主執行緒 === 3 執行緒1 — 3 執行緒2 — 3 主執行緒 === 4 執行緒1 — 4 請按任意鍵繼續… 執行緒2 — 4 執行緒2 — 5 執行緒2 — 6 執行緒2 — 7 執行緒2 — 8 執行緒2 — 9 |
為進一步理解執行緒同步的重要性和互斥量的使用方法,我們再來看一個例子。
買火車票是大家春節回家最為關注的事情,我們就簡單模擬一下火車票的售票系統(為使程式簡單,我們就抽出最簡單的模型進行模擬):有500張從北京到贛州的火車票,在8個視窗同時出售,保證系統的穩定性和資料的原子性。
【Demo4】:模擬火車售票系統
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#include "stdafx.h" #include <windows.h> #include <iostream> #include <strstream> #include <string> using namespace std; #define NAME_LINE 40 //定義執行緒函式傳入引數的結構體 typedef struct __TICKET { int nCount; char strTicketName[NAME_LINE]; __TICKET() : nCount(0) { memset(strTicketName, 0, NAME_LINE * sizeof(char)); } }TICKET; typedef struct __THD_DATA { TICKET* pTicket; char strThreadName[NAME_LINE]; __THD_DATA() : pTicket(NULL) { memset(strThreadName, 0, NAME_LINE * sizeof(char)); } }THD_DATA; //基本型別資料轉換成字串 template<class T> string convertToString(const T val) { string s; std::strstream ss; ss << val; ss >> s; return s; } //售票程式 DWORD WINAPI SaleTicket(LPVOID lpParameter); |
SaleTickets.cpp :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include "stdafx.h" #include <windows.h> #include <iostream> #include "SaleTickets.h" using namespace std; extern HANDLE g_hMutex; //售票程式 DWORD WINAPI SaleTicket(LPVOID lpParameter) { THD_DATA* pThreadData = (THD_DATA*)lpParameter; TICKET* pSaleData = pThreadData->pTicket; while(pSaleData->nCount > 0) { //請求獲得一個互斥量鎖 WaitForSingleObject(g_hMutex, INFINITE); if (pSaleData->nCount > 0) { cout << pThreadData->strThreadName << "出售第" << pSaleData->nCount -- << "的票,"; if (pSaleData->nCount >= 0) { cout << "出票成功!剩餘" << pSaleData->nCount << "張票." << endl; } else { cout << "出票失敗!該票已售完。" << endl; } } Sleep(10); //釋放互斥量鎖 ReleaseMutex(g_hMutex); } return 0L; } |
測試程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//售票系統 void Test2() { //建立一個互斥量 g_hMutex = CreateMutex(NULL, FALSE, NULL); //初始化火車票 TICKET ticket; ticket.nCount = 100; strcpy(ticket.strTicketName, "北京-->贛州"); const int THREAD_NUMM = 8; THD_DATA threadSale[THREAD_NUMM]; HANDLE hThread[THREAD_NUMM]; for(int i = 0; i < THREAD_NUMM; ++ i) { threadSale[i].pTicket = &ticket; string strThreadName = convertToString(i); strThreadName = "視窗" + strThreadName; strcpy(threadSale[i].strThreadName, strThreadName.c_str()); //建立執行緒 hThread[i] = CreateThread(NULL, NULL, SaleTicket, &threadSale[i], 0, NULL); //請求獲得一個互斥量鎖 WaitForSingleObject(g_hMutex, INFINITE); cout << threadSale[i].strThreadName << "開始出售 " << threadSale[i].pTicket->strTicketName << " 的票..." << endl; //釋放互斥量鎖 ReleaseMutex(g_hMutex); //關閉執行緒 CloseHandle(hThread[i]); } system("pause"); } |
結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
視窗0開始出售 北京–>贛州 的票… 視窗0出售第100的票,出票成功!剩餘99張票. 視窗1開始出售 北京–>贛州 的票… 視窗1出售第99的票,出票成功!剩餘98張票. 視窗0出售第98的票,出票成功!剩餘97張票. 視窗2開始出售 北京–>贛州 的票… 視窗2出售第97的票,出票成功!剩餘96張票. 視窗1出售第96的票,出票成功!剩餘95張票. 視窗0出售第95的票,出票成功!剩餘94張票. 視窗3開始出售 北京–>贛州 的票… 視窗3出售第94的票,出票成功!剩餘93張票. 視窗2出售第93的票,出票成功!剩餘92張票. 視窗1出售第92的票,出票成功!剩餘91張票. 視窗0出售第91的票,出票成功!剩餘90張票. 視窗4開始出售 北京–>贛州 的票… 視窗4出售第90的票,出票成功!剩餘89張票. 視窗3出售第89的票,出票成功!剩餘88張票. 視窗2出售第88的票,出票成功!剩餘87張票. 視窗1出售第87的票,出票成功!剩餘86張票. 視窗0出售第86的票,出票成功!剩餘85張票. 視窗5開始出售 北京–>贛州 的票… 視窗5出售第85的票,出票成功!剩餘84張票. 視窗4出售第84的票,出票成功!剩餘83張票. 視窗3出售第83的票,出票成功!剩餘82張票. 視窗2出售第82的票,出票成功!剩餘81張票. |