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