C++实现信号量


toc

背景

信号量与条件变量差异对比

  • 信号量存在一个计数,可以反映出当前阻塞在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同样也会有此问题

坑的处理

  1. 使用老版本C++标准库暂时无解,只能建议在使用条件变量时,暂时不使用定时等待方式,但根据具体平台还是可以处理因为系统时间改变,而引起的定时等待坑
    • Linux下
      • 使用pthread_mutex_t和pthread_cond_t、pthread_condattr_t,将条件变量的时钟属性设置为CLOCK_MONOTONIC,并以设置好的属性初始化条件变量,即可避免系统时间改变带来影响
    • Windows下
      • 使用Win32 API CreateSemaphore创建信号量,并配合WaitForSingleObject实现超时条件等待,经测试,是可以处理此问题的
        Windows信号量使用范例使用信号量对象

2.新版本C++标准库部分可行

3.使用1.67以上boost 版本

通过源码,我们可以发现两个平台均已处理此情况






免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM