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