背景
信號量與條件變量差異對比
- 信號量存在一個計數,可以反映出當前阻塞在wait上的線程數(值小於0),或下次wait不會阻塞的線程數;條件變量沒有相應計數
- 信號量僅能遞增或遞減計數,信號量每次遞增只能喚醒一個阻塞線程;條件變量存在廣播操作,能一次性喚醒所有阻塞線程
- 信號量計數可以被初始化為大於0的數n,在后續訪問時wait時,n個線程均不會阻塞,可同時訪問資源;條件變量初始化后,執行wait的線程將全部阻塞,直到收到通知
- 不存在阻塞線程時,信號量遞增一次,便會多一個wait不阻塞的線程;對於條件變量,當沒有線程阻塞在wait時,發出的喚醒信號將被丟棄,導致先發出喚醒信號,隨后wait將仍被阻塞,即喚醒丟失
- 信號量不存在虛假喚醒問題;條件變量存在虛假喚醒
- 信號量可單獨使用;條件變量必須需配合mutex一起使用
C++標准庫僅有條件變量,而沒有信號量,下面實現一個跨平台的用於線程間同步的信號量
實現
信號量最基本的操作有三個
- 初始化決定了wait后可立即執行線程數,一般代表可訪問資源個數
- 遞減操作SemWait,該操作使信號量減1,如果減1后變為負數,線程會阻塞在SemWait上,否則繼續執行
- 遞增操作SemSignal,該操作使信號量加1,如果加1后大於等於0,阻塞在SemWait上的線程被喚醒
使用條件變量 + 鎖 + 計數的方式實現信號量
- wait的線程執行阻塞前,會對計數進行判斷,決定是否繼續執行,由此處理喚醒丟失問題
- wait的線程被喚醒時,檢查計數,決定是否繼續阻塞,由此處理虛假喚醒問題
代碼
#ifndef _SEMAPHORE_H_
#define _SEMAPHORE_H_
#include <mutex>
#include <chrono>
#include <condition_variable>
class Semaphore final{
public:
explicit Semaphore(int iCount = 0);
~Semaphore();
void Signal();
void Wait();
bool TryWait();
int GetValue();
void TimedWait(std::chrono::milliseconds milliseconds);
Semaphore(const Semaphore& rhs) = delete;
Semaphore(Semaphore&& rhs) = delete;
Semaphore& operator=(const Semaphore& rhs) = delete;
Semaphore& operator=(Semaphore&& rhs) = delete;
private:
std::mutex m_mLock;
std::condition_variable m_cConditionVariable;
int m_iCount; //大於等於0時表示,可訪問資源數/小於0時,絕對值表示阻塞線程數
};
#endif // !_SEMAPHORE_H_
#include "Semaphore.h"
Semaphore::Semaphore(int iCount) : m_iCount(iCount){
}
Semaphore::~Semaphore(){
}
void Semaphore::Signal(){
int iCount = 0;
std::unique_lock<std::mutex> lock(m_mLock);
iCount = ++m_iCount;
lock.unlock(); //提前unlock,讓Wait線程被喚醒后能及時拿到鎖
if(iCount <= 0){ //存在阻塞線程時才notify,避免系統調用開銷
m_cConditionVariable.notify_one();
}
}
void Semaphore::Wait(){
std::unique_lock<std::mutex> lock(m_mLock);
m_iCount--;
m_cConditionVariable.wait(lock, [this] { return m_iCount >= 0; }); //lambda是處理喚醒丟失和虛假喚醒的關鍵
}
bool Semaphore::TryWait(){
std::unique_lock<std::mutex> lock(m_mLock);
if(m_iCount <= 0){
return false;
}
m_iCount--;
return true;
}
int Semaphore::GetValue(){
std::unique_lock<std::mutex> lock(m_mLock);
return m_iCount;
}
void Semaphore::TimedWait(std::chrono::milliseconds milliseconds){
std::unique_lock<std::mutex> lock(m_mLock);
m_iCount--;
m_cConditionVariable.wait_for(lock, milliseconds, [this] { return m_iCount >= 0; }); //條件變量定時等待有坑
}
條件變量與鎖
必須有鎖的原因:
- 達成互斥:條件變量典型使用場景為:生產消費者模型,兩者共享同一內存空間,(可能多個)生產者生產數據到共享空間,(可能多個)消費者從共享空間消費數據,為避免生產時與消費時的數據競爭,保證數據正確性,需要進行互斥,所以必須要mutex
標准庫中條件變量對鎖的操作
- 消費者在執行條件變量的Wait后,會阻塞並釋放鎖,使生產者可以向共享空間生產數據
- 待生產者通知,被Wait阻塞的消費者會被喚醒,並試圖獲取鎖,獲得對數據的獨占機會
條件變量定時等待的坑
std::condition_variable的wait_for方法由wait_until實現,並且依賴的時間是系統時間,經測試,不管是在windows還是在Linux,只要系統時間改變,就會導致等待時間與預想時間不符!!!!!假設意圖等待10秒
- 時間回跳:時間回跳1分鍾,不管是LInux還是Windows,等待的時間都將會有1分鍾多一點(多多少,取決於更改時間手速)
- 時間后撥:Linux下,會立刻完成超時,Windows下正常等待10秒
測試環境如下:
Linux + g++7.5
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
wait_for實現
template<typename _Rep, typename _Period>
cv_status
wait_for(unique_lock<mutex>& __lock,
const chrono::duration<_Rep, _Period>& __rtime)
{
using __dur = typename __clock_t::duration;
auto __reltime = chrono::duration_cast<__dur>(__rtime);
if (__reltime < __rtime)
++__reltime;
return wait_until(__lock, __clock_t::now() + __reltime);
}
template<typename _Rep, typename _Period, typename _Predicate>
bool
wait_for(unique_lock<mutex>& __lock,
const chrono::duration<_Rep, _Period>& __rtime,
_Predicate __p)
{
using __dur = typename __clock_t::duration;
auto __reltime = chrono::duration_cast<__dur>(__rtime);
if (__reltime < __rtime)
++__reltime;
return wait_until(__lock, __clock_t::now() + __reltime, std::move(__p));
}
可以看到wait_for依賴了wait_until,並等待到以__clock_t::now()為基准偏移的時間,而__clock_t 如下:
/// condition_variable
class condition_variable
{
typedef chrono::system_clock __clock_t;
//......
__clock_t::now()就是系統時間,從源碼上也可以看到調整系統時間會給wait_for帶來影響
WIndows + VS2017
wait_for實現
emplate<class _Rep,
class _Period>
cv_status wait_for(
unique_lock<mutex>& _Lck,
const chrono::duration<_Rep, _Period>& _Rel_time)
{ // wait for duration
_STDEXT threads::xtime _Tgt = _To_xtime(_Rel_time);
return (wait_until(_Lck, &_Tgt));
}
template<class _Rep,
class _Period,
class _Predicate>
bool wait_for(
unique_lock<mutex>& _Lck,
const chrono::duration<_Rep, _Period>& _Rel_time,
_Predicate _Pred)
{ // wait for signal with timeout and check predicate
_STDEXT threads::xtime _Tgt = _To_xtime(_Rel_time);
return (_Wait_until1(_Lck, &_Tgt, _Pred));
}
可以看到,VS下wait_for也依賴了wait_until,讓我們在看看_To_xtime是什么
template<class _Rep,
class _Period> inline
xtime _To_xtime(const chrono::duration<_Rep, _Period>& _Rel_time)
{ // convert duration to xtime
xtime _Xt;
if (_Rel_time <= chrono::duration<_Rep, _Period>::zero())
{ // negative or zero relative time, return zero
_Xt.sec = 0;
_Xt.nsec = 0;
}
else
{ // positive relative time, convert
chrono::nanoseconds _T0 =
chrono::system_clock::now().time_since_epoch();
_T0 += chrono::duration_cast<chrono::nanoseconds>(_Rel_time);
_Xt.sec = chrono::duration_cast<chrono::seconds>(_T0).count();
_T0 -= chrono::seconds(_Xt.sec);
_Xt.nsec = (long)_T0.count();
}
return (_Xt);
}
也是基於系統時間,wait_for同樣也會有此問題
坑的處理
- 使用老版本C++標准庫暫時無解,只能建議在使用條件變量時,暫時不使用定時等待方式,但根據具體平台還是可以處理因為系統時間改變,而引起的定時等待坑
- Linux下
- 使用pthread_mutex_t和pthread_cond_t、pthread_condattr_t,將條件變量的時鍾屬性設置為CLOCK_MONOTONIC,並以設置好的屬性初始化條件變量,即可避免系統時間改變帶來影響
- Windows下
- 使用Win32 API CreateSemaphore創建信號量,並配合WaitForSingleObject實現超時條件等待,經測試,是可以處理此問題的
Windows信號量使用范例使用信號量對象
- 使用Win32 API CreateSemaphore創建信號量,並配合WaitForSingleObject實現超時條件等待,經測試,是可以處理此問題的
- Linux下
2.新版本C++標准庫部分可行
- Linux下 可以使用g++10及以上版本Bug 41861 (DR887) - [DR 887][C++0x]
does not use monotonic_clock - Windows下 仍未處理
// TRANSITION, ABI: The standard says that we should use a steady clock,
// but unfortunately our ABI speaks struct xtime, which is relative to the system clock.
來源: https://github.com/microsoft/STL/blob/main/stl/inc/condition_variable
3.使用1.67以上boost 版本
- Linux https://github.com/boostorg/thread/blob/boost-1.67.0/include/boost/thread/pthread/condition_variable.hpp
- WIndows https://github.com/boostorg/thread/blob/boost-1.67.0/include/boost/thread/win32/condition_variable.hpp
通過源碼,我們可以發現兩個平台均已處理此情況