1、條件變量
條件變量(condition variable)是利用線程間共享的全局變量進行同步的一種機制,主要包括兩個動作:一個線程等待某個條件爲真,而將自己掛起;另一個線程使的條件成立,並通知等待的線程繼續。爲了防止競爭,條件變量的使用總是和一個互斥鎖結合在一起。
2、windows條件變量的實現
(1)利用同步對象實現條件變量
自己封裝的一個條件變量:
1 #ifndef _MY_CONDITION_H 2 #define _MY_CONDITION_H 3 4 #include <windows.h> 5 6 class MyCondition 7 { 8 public: 9 MyCondition() 10 { 11 m_hEvent = CreateEvent(NULL,TRUE,FALSE, NULL); 12 } 13 14 ~MyCondition() 15 { 16 ::CloseHandle(m_hEvent); 17 } 18 19 void ResetSignal() 20 { 21 ResetEvent(m_hEvent); 22 } 23 24 void Active() 25 { 26 ::SetEvent(m_hEvent); 27 } 28 29 bool timed_wait(int nSecond) 30 { 31 bool bRet=true; 32 if(INFINITE == nSecond) 33 { 34 if (WAIT_OBJECT_0 != ::WaitForSingleObject(m_hEvent, INFINITE)) 35 bRet=false; 36 } 37 else 38 { 39 if (WAIT_OBJECT_0 != ::WaitForSingleObject(m_hEvent, nSecond*1000)) 40 bRet=false; 41 } 42 43 ResetEvent(m_hEvent); 44 return bRet; 45 } 46 47 private: 48 HANDLE m_hEvent; 49 }; 50 51 #endif
CreateEvent時,第二個參數設置手動和自動兩種模式,這兩種模式將影響SetEvent、ResetEvent和PulseEvent操作,分別如下:
自動模式 |
手動模式 |
|
SetEvent |
將Event調爲激發態,放過一個等待線程,而後自動調回非激發態 |
一直放過等待 將Event調爲激發態 |
ResetEvent |
無用 |
停止放過線程 將Event調爲非激發態 |
PulseEvent |
這種模式下等同與SetEvent |
將Event調爲激發態,放過所有的等待的線程,然後調回非激發態 |
(2)CONDITION_VARIABLE
微軟從vista和2008以後引入的技術,xp和2003的系統不支持,相關的操作函數如下:
l WakeConditionVariable 喚醒一個等待條件變量的線程
l WakeAllConditionVariable 喚醒所有等待條件變量的線程;
l SleepConditionVariableCS 釋放臨界區鎖和等待條件變量作爲原子性操作
l SleepConditionVariableSRW 釋放SRW鎖和等待條件變量作爲原子性操作。
3、linux條件變量的實現
自己封裝的一個條件變量:
1 #ifndef _MY_CONDITION_H 2 #define _MY_CONDITION_H 3 4 #include <pthread.h> 5 #include <time.h> 6 #define INFINITE 0xFFFFFFFF 7 8 9 class MyCondition 10 { 11 public: 12 MyCondition() 13 { 14 pthread_condattr_t cond; 15 pthread_condattr_init(&cond); 16 pthread_condattr_setclock(&cond, CLOCK_MONOTONIC); 17 pthread_cond_init(&m_pthCondt, &cond); 18 pthread_mutex_init(&m_mtxCondt, NULL); 19 } 20 21 ~MyCondition() 22 { 23 pthread_cond_destroy(&m_pthCondt); 24 pthread_mutex_destroy(&m_mtxCondt); 25 } 26 27 void ResetSignal() 28 { 29 } 30 31 void Active() 32 { 33 pthread_mutex_lock(&m_mtxCondt); 34 pthread_cond_broadcast(&m_pthCondt); 35 pthread_mutex_unlock(&m_mtxCondt); 36 } 37 38 bool timed_wait(int nSecond) 39 { 40 bool bRet = true; 41 if (INFINITE == nSecond) 42 { 43 pthread_mutex_lock(&m_mtxCondt); 44 if (0 != pthread_cond_wait(&m_pthCondt, &m_mtxCondt)) 45 bRet = false; 46 pthread_mutex_unlock(&m_mtxCondt); 47 } 48 else 49 { 50 pthread_mutex_lock(&m_mtxCondt); 51 struct timespec tv; 52 clock_gettime(CLOCK_MONOTONIC, &tv); 53 tv.tv_sec += nSecond; 54 if (0 != pthread_cond_timedwait(&m_pthCondt, &m_mtxCondt, &tv)) 55 bRet = false; 56 pthread_mutex_unlock(&m_mtxCondt); 57 } 58 59 return bRet; 60 } 61 62 private: 63 pthread_cond_t m_pthCondt; 64 pthread_mutex_t m_mtxCondt; 65 }; 66 67 #endif
Linux提供了的條件等待函數和notify函數。
l pthread_cond_timedwait(cond, mutex, abstime);
l pthread_cond_wait(cond, mutex);
l pthread_cond_signal(cond); 將至少解鎖一個線程(阻塞在條件變量上的線程)。
l pthread_cond_broadcast(cond) : 將對所有阻塞在條件變量上的線程解鎖。
pthread_cond_wait() 所做的事包含三個部分:
1)同時對mutex解鎖
2)並等待條件 cond 發生
3)獲得通知後,對mutex加鎖
4、虛假喚醒
喚醒操作(SetEvent和pthread_cond_signal)原本意圖是喚醒一個等待的線程,但是在多核處理器下,可能會激活多個等待的線程,這種效應爲“虛假喚醒”。linux幫助文檔中提到:雖然虛假喚醒在pthread_cond_wait函數中可以解決,爲了發生概率很低的情況而降低邊緣條件(fringe condition)效率是不值得的,糾正這個問題會降低對所有基於它的所有更高級的同步操作的併發度。所以pthread_cond_wait的實現上沒有去解決它。所以通常的解決辦法是在線程被激活後還需要檢測等待的條件是否滿足,例如下圖所示。
pthread_cond_wait中的while()不僅僅在等待條件變量前檢查條件,實際上在等待條件變量後也檢查條件。
5、喚醒丟失
如果在等待條件變量(pthread_cond_wait)前,條件變量就被喚醒激活(pthread_cond_signal),那麼這次喚醒就會丟失。
例如客戶端向服務端發送同步消息時,客戶端需要等到服務的迴應再返回發送接口,這時需要在發送接口內部等待迴應。
利用條件變量實現如下:
1 //ClientSession.h 2 #ifndef _CLIENT_SESSION_H_ 3 #define _CLIENT_SESSION_H_ 4 5 class ClientSession 6 { 7 public: 8 ClientSession(); 9 ~ClientSession(); 10 11 public: 12 void OnRecvServerResponse(); 13 14 public: 15 int SendSyncMsg(string sMsg); 16 17 private: 18 MyCondition m_objCond; 19 TcpServer m_objTcpServer; 20 }; 21 22 #endif
1 //ClientSession.cpp 2 3 #include "ClientSession.h" 4 5 ClientSession::ClientSession() 6 { 7 } 8 9 ClientSession::~ClientSession() 10 { 11 } 12 13 void ClientSession::OnRecvServerResponse() 14 { 15 m_objCond.Active(); //喚醒等待 16 } 17 18 int SendSyncMsg(string sMsg) 19 { 20 long lRet = m_objTcpServer.SendData(sMsg.c_str(),sMsg.length()); 21 if (lRet >= 0) //網絡發送成功 22 { 23 if (m_objCond.time_wait(5000) >= 0) //收到服務端迴應 24 { 25 return 0; //發送成功 26 } 27 else //服務端迴應超時 28 { 29 return -1; 30 } 31 } 32 return -1; 33 }
SendSyncMsg中存在問題,如果在SendData之後,m_objCond.time_wait(5000)之前,m_objCond.Active()被調用,則會出現喚醒丟失。
喚醒丟失問題可以採用信號量來解決。
6、C++11中條件變量
從C++11之後,c++標準庫實現了條件變量,具體可以參考http://www.cplusplus.com/reference/condition_variable/condition_variable/?kw=condition_variable
7、信號量
信號量包含一個信號值,在windows和linux中實現如下:
1 #ifndef _TIME_SEM_H_ 2 #define _TIME_SEM_H_ 3 4 #include <iostream> 5 #include <assert.h> 6 using namespace std; 7 8 #ifdef _MSC_VER 9 #define WIN32_LEAN_AND_MEAN // 從 Windows 頭中排除極少使用的資料 10 11 # if !defined(_WINDOWS_) 12 # include <windows.h> 13 # include <winbase.h> 14 # endif 15 16 #elif defined(__GNUC__) 17 # include <pthread.h> 18 # include <unistd.h> 19 # include <sys/time.h> 20 # include <errno.h> 21 # ifndef __bsdi__ 22 # include <semaphore.h> 23 # endif 24 #endif 25 26 27 28 #ifdef _MSC_VER 29 30 class TimeSem 31 { 32 public: 33 TimeSem(int intial_count = 0) 34 { 35 m_sem = CreateSemaphore(NULL, intial_count, 65535, NULL); 36 } 37 ~TimeSem() { CloseHandle(m_sem); } 38 bool Wait(int timeout = INFINITE) 39 { 40 return WaitForSingleObject(m_sem, timeout) == WAIT_OBJECT_0; 41 } 42 bool TryWait() { return Wait(0); } 43 void Signal() { ReleaseSemaphore(m_sem, 1, NULL); } 44 45 protected: 46 HANDLE m_sem; 47 }; 48 49 #elif defined(__GNUC__) 50 51 class TimeSem 52 { 53 public: 54 TimeSem(int intial_count = 0) : m_count(intial_count) 55 { 56 pthread_cond_init(&m_cond, NULL); 57 pthread_mutex_init(&m_mtx, NULL); 58 pipe(m_fds); 59 if (m_count > 0) 60 { 61 for (int i = 0; i < intial_count; i++) 62 { 63 char buf[] = "semaphore: signal"; 64 write(m_fds[1], buf, sizeof(buf)); 65 } 66 } 67 } 68 ~TimeSem() 69 { 70 pthread_cond_destroy(&m_cond); 71 pthread_mutex_destroy(&m_mtx); 72 close(m_fds[0]); close(m_fds[1]); 73 } 74 bool Wait(int timeout = -1) 75 { 76 //pthread_mutex_lock(&m_mtx); 77 78 fd_set rset; 79 FD_ZERO(&rset); 80 FD_SET(m_fds[0], &rset); 81 82 int ret = -1; 83 if (timeout < 0) 84 ret = select(m_fds[0] + 1, &rset, NULL, NULL, NULL); 85 else 86 { 87 timeval tv; 88 89 tv.tv_sec = timeout / 1000; 90 tv.tv_usec = (timeout % 1000) * 1000; 91 92 ret = select(m_fds[0] + 1, &rset, NULL, NULL, &tv); 93 } 94 95 if (ret == 1 && FD_ISSET(m_fds[0], &rset)) 96 { 97 if (--m_count >= 0); 98 { 99 char buf[] = "semaphore: signal"; 100 read(m_fds[0], buf, sizeof(buf)); 101 } 102 } 103 else 104 { 105 //pthread_mutex_unlock(&m_mtx); 106 return false; 107 } 108 109 //pthread_mutex_unlock(&m_mtx); 110 return true; 111 } 112 bool TryWait() 113 { 114 bool res = false; 115 pthread_mutex_lock(&m_mtx); 116 res = m_count > 0; 117 if (m_count > 0) 118 { 119 char buf[] = "semaphore: signal"; 120 read(m_fds[0], buf, sizeof(buf)); 121 --m_count; 122 } 123 pthread_mutex_unlock(&m_mtx); 124 return res; 125 } 126 void release() 127 { 128 //pthread_mutex_lock(&m_mtx); 129 if (++m_count > 0) 130 { 131 char buf[] = "semaphore: signal"; 132 write(m_fds[1], buf, sizeof(buf)); 133 } 134 //pthread_mutex_unlock(&m_mtx); 135 } 136 void signal() { release(); } 137 138 protected: 139 pthread_cond_t m_cond; 140 pthread_mutex_t m_mtx; 141 int m_count; 142 int m_fds[2]; 143 }; 144 145 #endif 146 147 #endif
8、信號量和條件變量的區別和聯繫
(1)使用條件變量可以一次喚醒所有等待者,而這個信號量沒有的功能,感覺是最大區別。
(2)信號量是有一個值(狀態的),而條件變量是沒有的,沒有地方記錄喚醒(發送信號)過多少次,也沒有地方記錄喚醒線程(wait返回)過多少次。從實現上來說一個信號量可以是用mutex + counter + condition variable實現的。因爲信號量有一個狀態,如果想精準的同步,那麼信號量可能會有特殊的地方。信號量可以解決條件變量中存在的喚醒丟失問題。
(3)在Posix.1基本原理一文聲稱,有了互斥鎖和條件變量還提供信號量的原因是:“本標準提供信號量的而主要目的是提供一種進程間同步的方式;這些進程可能共享也可能不共享內存區。互斥鎖和條件變量是作爲線程間的同步機制說明的;這些線程總是共享(某個)內存區。這兩者都是已廣泛使用了多年的同步方式。每組原語都特別適合於特定的問題”。儘管信號量的意圖在於進程間同步,互斥鎖和條件變量的意圖在於線程間同步,但是信號量也可用於線程間,互斥鎖和條件變量也可用於進程間。應當根據實際的情況進行決定。信號量最有用的場景是用以指明可用資源的數量。
參考:
https://blog.csdn.net/puncha/article/details/8493862
https://blog.csdn.net/hemmanhui/article/details/4417433
https://blog.csdn.net/fullsail/article/details/8607566