線程同步是指同一進程中的多個線程互相協調工作從而達到一致性。之所以需要線程同步,是因為多個線程同時對一個數據對象進行修改操作時,可能會對數據造成破壞,下面是多個線程同時修改同一數據造成破壞的例子:
1 #include <thread> 2 #include <iostream> 3 4 void Fun_1(unsigned int &counter); 5 void Fun_2(unsigned int &counter); 6 7 int main() 8 { 9 unsigned int counter = 0; 10 std::thread thrd_1(Fun_1, counter); 11 std::thread thrd_2(Fun_2, counter); 12 thrd_1.join(); 13 thrd_2.join(); 14 system("pause"); 15 return 0; 16 } 17 18 void Fun_1(unsigned int &counter) 19 { 20 while (true) 21 { 22 ++counter; 23 if (counter < 1000) 24 { 25 std::cout << "Function 1 counting " << counter << "...\n"; 26 } 27 else 28 { 29 break; 30 } 31 } 32 } 33 34 void Fun_2(unsigned int &counter) 35 { 36 while (true) 37 { 38 ++counter; 39 if (counter < 1000) 40 { 41 std::cout << "Function 2 counting " << counter << "...\n"; 42 } 43 else 44 { 45 break; 46 } 47 } 48 }
運行結果如圖所示:
顯然輸出的結果存在問題,變量並沒有按順序遞增,所以線程同步是很重要的。在這里記錄三種線程同步的方式:
①使用C++標准庫的thread、mutex頭文件:
1 #include <thread> 2 #include <mutex> 3 #include <iostream> 4 5 void Fun_1(); 6 void Fun_2(); 7 8 unsigned int counter = 0; 9 std::mutex mtx; 10 11 int main() 12 { 13 std::thread thrd_1(Fun_1); 14 std::thread thrd_2(Fun_2); 15 thrd_1.join(); 16 thrd_2.join(); 17 system("pause"); 18 return 0; 19 } 20 21 void Fun_1() 22 { 23 while (true) 24 { 25 std::lock_guard<std::mutex> mtx_locker(mtx); 26 ++counter; 27 if (counter < 1000) 28 { 29 std::cout << "Function 1 counting " << counter << "...\n"; 30 } 31 else 32 { 33 break; 34 } 35 } 36 } 37 38 void Fun_2() 39 { 40 while (true) 41 { 42 std::lock_guard<std::mutex> mtx_locker(mtx); 43 ++counter; 44 if (counter < 1000) 45 { 46 std::cout << "Function 2 counting " << counter << "...\n"; 47 } 48 else 49 { 50 break; 51 } 52 } 53 }
這段代碼與前面一段代碼唯一的區別就是在兩個線程關聯的函數中加了一句 std::lock_guard<std::mutex> mtx_locker(mtx); 在C++中,通過構造std::mutex的實例來創建互斥元,可通過調用其成員函數lock()和unlock()來實現加鎖和解鎖,然后這是不推薦的做法,因為這要求程序員在離開函數的每條代碼路徑上都調用unlock(),包括由於異常所導致的在內。作為替代,標准庫提供了std::lock_guard類模板,實現了互斥元的RAII慣用語法(資源獲取即初始化)。該對象在構造時鎖定所給的互斥元,析構時解鎖該互斥元,從而保證被鎖定的互斥元始終被正確解鎖。代碼運行結果如下圖所示,可見得到了正確的結果。
②使用windows API的臨界區對象:
1 //header.h 2 #ifndef CRTC_SEC_H 3 #define CRTC_SEC_H 4 5 #include "windows.h" 6 7 class RAII_CrtcSec 8 { 9 private: 10 CRITICAL_SECTION crtc_sec; 11 public: 12 RAII_CrtcSec() { ::InitializeCriticalSection(&crtc_sec); } 13 ~RAII_CrtcSec() { ::DeleteCriticalSection(&crtc_sec); } 14 RAII_CrtcSec(const RAII_CrtcSec &) = delete; 15 RAII_CrtcSec & operator=(const RAII_CrtcSec &) = delete; 16 // 17 void Lock() { ::EnterCriticalSection(&crtc_sec); } 18 void Unlock() { ::LeaveCriticalSection(&crtc_sec); } 19 }; 20 21 #endif
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 #include "header.h" 5 6 DWORD WINAPI Fun_1(LPVOID p); 7 DWORD WINAPI Fun_2(LPVOID p); 8 9 unsigned int counter = 0; 10 RAII_CrtcSec cs; 11 12 int main() 13 { 14 HANDLE h1, h2; 15 h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, 0); 16 std::cout << "Thread 1 started...\n"; 17 h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, 0); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 cs.Lock(); 31 ++counter; 32 if (counter < 1000) 33 { 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 cs.Unlock(); 36 } 37 else 38 { 39 cs.Unlock(); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 cs.Lock(); 51 ++counter; 52 if (counter < 1000) 53 { 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 cs.Unlock(); 56 } 57 else 58 { 59 cs.Unlock(); 60 break; 61 } 62 } 63 return 0; 64 }
上面的代碼使用了windows提供的API中的臨界區對象來實現線程同步。臨界區是指一個訪問共享資源的代碼段,臨界區對象則是指當用戶使用某個線程訪問共享資源時,必須使代碼段獨占該資源,不允許其他線程訪問該資源。在該線程訪問完資源后,其他線程才能對資源進行訪問。Windows API提供了臨界區對象的結構體CRITICAL_SECTION,對該對象的使用可總結為如下幾步:
1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是初始化臨界區,唯一的參數是指向結構體CRITICAL_SECTION的指針變量。
2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是使調用該函數的線程進入已經初始化的臨界區,並擁有該臨界區的所有權。這是一個阻塞函數,如果線程獲得臨界區的所有權成功,則該函數將返回,調用線程繼續執行,否則該函數將一直等待,這樣會造成該函數的調用線程也一直等待。如果不想讓調用線程等待(非阻塞),則應該使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。
3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是使調用該函數的線程離開臨界區並釋放對該臨界區的所有權,以便讓其他線程也獲得訪問該共享資源的機會。一定要在程序不適用臨界區時調用該函數釋放臨界區所有權,否則程序將一直等待造成程序假死。
4.DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是刪除程序中已經被初始化的臨界區。如果函數調用成功,則程序會將內存中的臨界區刪除,防止出現內存錯誤。
該段代碼的運行結果如下圖所示:
③使用Windows API的事件對象:
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_event; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_event = CreateEvent(nullptr, true, false, nullptr); 14 SetEvent(h_event); 15 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 16 std::cout << "Thread 1 started...\n"; 17 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 18 std::cout << "Thread 2 started...\n"; 19 CloseHandle(h1); 20 CloseHandle(h2); 21 // 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_event, INFINITE); 31 ResetEvent(h_event); 32 if (counter < 1000) 33 { 34 ++counter; 35 std::cout << "Thread 1 counting " << counter << "...\n"; 36 SetEvent(h_event); 37 } 38 else 39 { 40 SetEvent(h_event); 41 break; 42 } 43 } 44 return 0; 45 } 46 47 DWORD WINAPI Fun_2(LPVOID p) 48 { 49 while (true) 50 { 51 WaitForSingleObject(h_event, INFINITE); 52 ResetEvent(h_event); 53 if (counter < 1000) 54 { 55 ++counter; 56 std::cout << "Thread 2 counting " << counter << "...\n"; 57 SetEvent(h_event); 58 } 59 else 60 { 61 SetEvent(h_event); 62 break; 63 } 64 } 65 return 0; 66 }
事件對象是一種內核對象,用戶在程序中使用內核對象的有無信號狀態來實現線程的同步。使用事件對象的步驟可概括如下:
1.創建事件對象,函數原型為:
1 HANDLE WINAPI CreateEvent( 2 _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, 3 _In_ BOOL bManualReset, 4 _In_ BOOL bInitialState, 5 _In_opt_ LPCTSTR lpName 6 );
如果該函數調用成功,則返回新創建的事件對象,否則返回NULL。函數參數的含義如下:
-lpEventAttributes:表示創建的事件對象的安全屬性,若設為NULL則表示該程序使用的是默認安全屬性。
-bManualReset:表示所創建的事件對象是人工重置還是自動重置。若設為true,則表示使用人工重置,在調用線程獲得事件對象所有權后用戶要顯式地調用ResetEvent()將事件對象設置為無信號狀態。
-bInitialState:表示事件對象的初始狀態。如果為true,則表示該事件對象初始時為有信號狀態,則線程可以使用事件對象。
-lpName:表示事件對象的名稱,若為NULL,則表示創建的是匿名事件對象。
2.若事件對象初始狀態設置為無信號,則需調用SetEvent(HANDLE hEvent)將其設置為有信號狀態。ResetEvent(HANDLE hEvent)則用於將事件對象設置為無信號狀態。
3.線程通過調用WaitForSingleObject()主動請求事件對象,該函數原型如下:
1 DWORD WINAPI WaitForSingleObject( 2 _In_ HANDLE hHandle, 3 _In_ DWORD dwMilliseconds 4 );
該函數將在用戶指定的事件對象上等待。如果事件對象處於有信號狀態,函數將返回。否則函數將一直等待,直到用戶所指定的事件到達。
該代碼的運行結果如下圖所示:
④使用Windows API的互斥對象:
1 //main.cpp 2 #include <windows.h> 3 #include <iostream> 4 5 DWORD WINAPI Fun_1(LPVOID p); 6 DWORD WINAPI Fun_2(LPVOID p); 7 8 HANDLE h_mutex; 9 unsigned int counter = 0; 10 11 int main() 12 { 13 h_mutex = CreateMutex(nullptr, false, nullptr); 14 HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr); 15 std::cout << "Thread 1 started...\n"; 16 HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr); 17 std::cout << "Thread 2 started...\n"; 18 CloseHandle(h1); 19 CloseHandle(h2); 20 // 21 //CloseHandle(h_mutex); 22 system("pause"); 23 return 0; 24 } 25 26 DWORD WINAPI Fun_1(LPVOID p) 27 { 28 while (true) 29 { 30 WaitForSingleObject(h_mutex, INFINITE); 31 if (counter < 1000) 32 { 33 ++counter; 34 std::cout << "Thread 1 counting " << counter << "...\n"; 35 ReleaseMutex(h_mutex); 36 } 37 else 38 { 39 ReleaseMutex(h_mutex); 40 break; 41 } 42 } 43 return 0; 44 } 45 46 DWORD WINAPI Fun_2(LPVOID p) 47 { 48 while (true) 49 { 50 WaitForSingleObject(h_mutex, INFINITE); 51 if (counter < 1000) 52 { 53 ++counter; 54 std::cout << "Thread 2 counting " << counter << "...\n"; 55 ReleaseMutex(h_mutex); 56 } 57 else 58 { 59 ReleaseMutex(h_mutex); 60 break; 61 } 62 } 63 return 0; 64 }
互斥對象的使用方法和c++標准庫的mutex類似,互斥對象使用完后應記得釋放。