線程同步是指同一進程中的多個線程互相協調工做從而達到一致性。之因此須要線程同步,是由於多個線程同時對一個數據對象進行修改操做時,可能會對數據形成破壞,下面是多個線程同時修改同一數據形成破壞的例子:ios
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++
顯然輸出的結果存在問題,變量並無按順序遞增,因此線程同步是很重要的。在這裏記錄三種線程同步的方式:程序員
①使用C++標準庫的thread、mutex頭文件:windows
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,對該對象的使用可總結爲以下幾步:spa
1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的做用是初始化臨界區,惟一的參數是指向結構體CRITICAL_SECTION的指針變量。線程
2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的做用是使調用該函數的線程進入已經初始化的臨界區,並擁有該臨界區的全部權。這是一個阻塞函數,若是線程得到臨界區的全部權成功,則該函數將返回,調用線程繼續執行,不然該函數將一直等待,這樣會形成該函數的調用線程也一直等待。若是不想讓調用線程等待(非阻塞),則應該使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。指針
3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的做用是使調用該函數的線程離開臨界區並釋放對該臨界區的全部權,以便讓其餘線程也得到訪問該共享資源的機會。必定要在程序不適用臨界區時調用該函數釋放臨界區全部權,不然程序將一直等待形成程序假死。code
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相似,互斥對象使用完後應記得釋放。