Windows下C++多執行緒程式設計(入門例項)
C++多執行緒併發程式設計視訊:https://pan.baidu.com/s/1cBGOgJmxb6wSJF-C3TvMbg
提取碼:opsm
多執行緒在程式設計中有相當重要的地位,我們在實際開發時或者找工作面試時總能遇到多執行緒的問題,對多執行緒的理解程度從一個側面反映了程式設計師的程式設計水平。
其實C++語言本身並沒有提供多執行緒機制(當然目前C++ 11新特性中,已經可以使用std::thread來建立執行緒了,因為還沒有系統地瞭解過,所以這裡不提了。),但Windows系統為我們提供了相關API,我們可以使用他們來進行多執行緒程式設計。
建立執行緒的API函式
1 HANDLE CreateThread(2 LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD:執行緒安全相關的屬性,常置為NULL 3 SIZE_T dwStackSize,//initialstacksize:新執行緒的初始化棧的大小,可設定為0 4 LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction:被執行緒執行的回撥函式,也稱為執行緒函式 5 LPVOID lpParameter,//threadargument:傳入執行緒函式的引數,不需傳遞引數時為NULL 6 DWORD dwCreationFlags,//creationoption:控制執行緒建立的標誌 7 LPDWORD lpThreadId//threadidentifier:傳出引數,用於獲得執行緒ID,如果為NULL則不返回執行緒ID 8 ) 9 10 /* 11 lpThreadAttributes:指向SECURITY_ATTRIBUTES結構的指標,決定返回的控制代碼是否可被子程序繼承,如果為NULL則表示返回的控制代碼不能被子程序繼承。 12 dwStackSize:設定初始棧的大小,以位元組為單位,如果為0,那麼預設將使用與呼叫該函式的執行緒相同的棧空間大小。 13 任何情況下,Windows根據需要動態延長堆疊的大小。14 lpStartAddress:指向執行緒函式的指標,函式名稱沒有限制,但是必須以下列形式宣告: 15 DWORD WINAPI 函式名 (LPVOID lpParam) ,格式不正確將無法呼叫成功。 16 lpParameter:向執行緒函式傳遞的引數,是一個指向結構的指標,不需傳遞引數時,為NULL。 17 dwCreationFlags:控制執行緒建立的標誌,可取值如下: 18 (1)CREATE_SUSPENDED(0x00000004):建立一個掛起的執行緒(就緒狀態),直到執行緒被喚醒時才呼叫 19 (2)0:表示建立後立即啟用。 20 (3)STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):dwStackSize引數指定初始的保留堆疊的大小, 21 如果STACK_SIZE_PARAM_IS_A_RESERVATION標誌未指定,dwStackSize將會設為系統預留的值 22 lpThreadId:儲存新執行緒的id 23 返回值:函式成功,返回執行緒控制代碼,否則返回NULL。如果執行緒建立失敗,可通過GetLastError函式獲得錯誤資訊。 24 */ 25 26 BOOL WINAPI CloseHandle(HANDLE hObject); //關閉一個被開啟的物件控制代碼 27 /*可用這個函式關閉建立的執行緒控制代碼,如果函式執行成功則返回true(非0),如果失敗則返回false(0), 28 如果執行失敗可呼叫GetLastError.函式獲得錯誤資訊。 29 */
多執行緒程式設計例項1:
1 #include <iostream> 2 #include <windows.h> 3 using namespace std; 4 5 DWORD WINAPI Fun(LPVOID lpParamter) 6 { 7 for (int i = 0; i < 10; i++) 8 cout << "A Thread Fun Display!" << endl; 9 return 0L; 10 } 11 12 int main() 13 { 14 HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL); 15 CloseHandle(hThread); 16 for (int i = 0; i < 10; i++) 17 cout << "Main Thread Display!" << endl; 18 return 0; 19 }
執行結果:
可以看到主執行緒(main函式)和我們自己的執行緒(Fun函式)是隨機交替執行的。可以看到Fun函式其實只運行了六次,這是因為主執行緒執行完之後將所佔資源都釋放掉了,使得子執行緒還沒有執行完。看來主執行緒執行得有點快,讓它sleep一下吧。
使用函式Sleep來暫停執行緒的執行。
1 VOID WINAPI Sleep( 2 __in DWORD dwMilliseconds 3 );
dwMilliseconds表示千分之一秒,所以 Sleep(1000); 表示暫停1秒。
多執行緒程式設計例項2:
1 #include <iostream> 2 #include <windows.h> 3 using namespace std; 4 5 DWORD WINAPI Fun(LPVOID lpParamter) 6 { 7 for (int i = 0; i < 10; i++) 8 { 9 cout << "A Thread Fun Display!" << endl; 10 Sleep(200); 11 } 12 13 return 0L; 14 } 15 16 int main() 17 { 18 HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL); 19 CloseHandle(hThread); 20 for (int i = 0; i < 10; i++) 21 { 22 cout << "Main Thread Display!" << endl; 23 Sleep(500); 24 } 25 26 return 0; 27 }
程式是每當Fun函式和main函式輸出內容後就會輸出換行,但是我們看到的確是有的時候程式輸出換行了,有的時候確沒有輸出換行,甚至有的時候是輸出兩個換行。這是怎麼回事?下面我們把程式改一下看看。
多執行緒程式設計例項3:
1 #include <iostream> 2 #include <windows.h> 3 using namespace std; 4 5 DWORD WINAPI Fun(LPVOID lpParamter) 6 { 7 for (int i = 0; i < 10; i++) 8 { 9 //cout << "A Thread Fun Display!" << endl; 10 cout << "A Thread Fun Display!\n"; 11 Sleep(200); 12 } 13 14 return 0L; 15 } 16 17 int main() 18 { 19 HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL); 20 CloseHandle(hThread); 21 for (int i = 0; i < 10; i++) 22 { 23 //cout << "Main Thread Display!" << endl; 24 cout << "Main Thread Display!\n"; 25 Sleep(500); 26 } 27 28 return 0; 29 }
這時候,正如我們預期的,正確地輸出了我們想要輸出的內容並且格式也是正確的。在這裡,我們可以把螢幕看成是一個資源,這個資源被兩個執行緒所共用,加入當Fun函式輸出了Fun Display!後,將要輸出endl(也就是清空緩衝區並換行,在這裡我們可以不用理解什麼是緩衝區),但此時,main函式卻得到了執行的機會,此時Fun函式還沒有來得及輸出換行(時間片用完),就把CPU讓給了main函式,而這時main函式就直接在Fun Display!後輸出Main Display!。
另一種情況就是“輸出兩個換行”,這種情況就是比如輸出Main Display!並輸出endl後,時間片用完,輪到子執行緒佔用CPU,子程序上一次時間片用完時停在了Fun Display!,下一次時間片過來時,剛好開始輸出endl,此時就會“輸出兩個換行”。
那麼為什麼我們把例項2改成例項3就可以正確的執行呢?原因在於,多個執行緒雖然是併發執行的,但是有一些操作(比如輸出一整段內容)是必須一氣呵成的,不允許打斷的,所以我們看到例項2和例項3的執行結果是不一樣的。它們之間的差異就是少了endl,而多了一個換行符\n。
那麼,是不是例項2的程式碼我們就不可以讓它正確的執行呢?答案當然是否定的,下面我就來講一下怎樣才能讓例項2的程式碼可以正確執行。這涉及到多執行緒的同步問題。對於一個資源被多個執行緒共用會導致程式的混亂,我們的解決方法是隻允許一個執行緒擁有對共享資源的獨佔,這裡我們用互斥量(Mutex)來進行執行緒同步。
在使用互斥量進行執行緒同步時,會用到以下幾個函式:
1 HANDLE WINAPI CreateMutex( 2 LPSECURITY_ATTRIBUTES lpMutexAttributes, //執行緒安全相關的屬性,常置為NULL 3 BOOL bInitialOwner, //建立Mutex時的當前執行緒是否擁有Mutex的所有權 4 LPCTSTR lpName //Mutex的名稱 5 ); 6 /* 7 MutexAttributes:也是表示安全的結構,與CreateThread中的lpThreadAttributes功能相同,表示決定返回的控制代碼是否可被子程序繼承,如果為NULL則表示返回的控制代碼不能被子程序繼承。 8 bInitialOwner:表示建立Mutex時的當前執行緒是否擁有Mutex的所有權,若為TRUE則指定為當前的建立執行緒為Mutex物件的所有者,其它執行緒訪問需要先ReleaseMutex 9 lpName:Mutex的名稱 10 */
1 DWORD WINAPI WaitForSingleObject( 2 HANDLE hHandle, //要獲取的鎖的控制代碼 3 DWORD dwMilliseconds //超時間隔 4 ); 5 6 /* 7 WaitForSingleObject:等待一個指定的物件(如Mutex物件),直到該物件處於非佔用的狀態(如Mutex物件被釋放)或超出設定的時間間隔。除此之外,還有一個與它類似的函式WaitForMultipleObjects,它的作用是等待一個或所有指定的物件,直到所有的物件處於非佔用的狀態,或超出設定的時間間隔。 8 hHandle:要等待的指定物件的控制代碼。 9 dwMilliseconds:超時的間隔,以毫秒為單位;如果dwMilliseconds為非0,則等待直到dwMilliseconds時間間隔用完或物件變為非佔用的狀態,如果dwMilliseconds 為INFINITE則表示無限等待,直到等待的物件處於非佔用的狀態。 10 */
1 BOOL WINAPI ReleaseMutex(HANDLE hMutex); 2 3 //說明:釋放所擁有的互斥量鎖物件,hMutex為釋放的互斥量控制代碼
多執行緒例項4:
1 #include <iostream> 2 #include <windows.h> 3 using namespace std; 4 5 HANDLE hMutex = NULL;//互斥量 6 //執行緒函式 7 DWORD WINAPI Fun(LPVOID lpParamter) 8 { 9 for (int i = 0; i < 10; i++) 10 { 11 //請求一個互斥量鎖 12 WaitForSingleObject(hMutex, INFINITE); 13 cout << "A Thread Fun Display!" << endl; 14 Sleep(100); 15 //釋放互斥量鎖 16 ReleaseMutex(hMutex); 17 } 18 return 0L;//表示返回的是long型的0 19 20 } 21 22 int main() 23 { 24 //建立一個子執行緒 25 HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL); 26 hMutex = CreateMutex(NULL, FALSE,"screen"); 27 //關閉執行緒控制代碼 28 CloseHandle(hThread); 29 //主執行緒的執行路徑 30 for (int i = 0; i < 10; i++) 31 { 32 //請求獲得一個互斥量鎖 33 WaitForSingleObject(hMutex,INFINITE); 34 cout << "Main Thread Display!" << endl; 35 Sleep(100); 36 //釋放互斥量鎖 37 ReleaseMutex(hMutex); 38 } 39 return 0; 40 }
注:CloseHandle是關閉執行緒控制代碼,用來釋放執行緒資源的,不是終止執行緒的。關閉執行緒控制代碼只是釋放控制代碼資源,新開啟執行緒後,如果不再利用其控制代碼,應該關閉控制代碼,釋放系統資源。關閉執行緒控制代碼和執行緒的結束與否沒有關係。如果主執行緒只想建立執行緒,而並不想之後再查詢或操縱它,那麼及時關閉控制代碼是個好習慣,免得當時沒關,以後又忘了,於是洩漏了系統的控制代碼資源(系統控制代碼總數是有限的)。