背景
信号量与条件变量差异对比
- 信号量存在一个计数,可以反映出当前阻塞在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
通过源码,我们可以发现两个平台均已处理此情况