C++11多線程-條件變量(std::condition_variable)

前面我們介紹了線程(std::thread)和互斥量(std::mutex),互斥量是多線程間同時(shí)訪問(wèn)某一共享變量時(shí),保證變量可被安全訪問(wèn)的手段。在多線程編程中柴钻,還有另一種十分常見(jiàn)的行為:線程同步。線程同步是指線程間需要按照預(yù)定的先后次序順序進(jìn)行的行為垢粮。C++11對(duì)這種行為也提供了有力的支持贴届,這就是條件變量。條件變量位于頭文件condition_variable下。本章我們將簡(jiǎn)要介紹一下該類(lèi)毫蚓,在文章的最后我們會(huì)綜合運(yùn)用std::mutex和std::condition_variable占键,實(shí)現(xiàn)一個(gè)chan類(lèi),該類(lèi)可在多線程間安全的通信元潘,具有廣泛的應(yīng)用場(chǎng)景畔乙。

1. std::condition_variable

條件變量提供了兩類(lèi)操作:wait和notify。這兩類(lèi)操作構(gòu)成了多線程同步的基礎(chǔ)翩概。

1.1 wait

wait是線程的等待動(dòng)作牲距,直到其它線程將其喚醒后,才會(huì)繼續(xù)往下執(zhí)行钥庇。下面通過(guò)偽代碼來(lái)說(shuō)明其用法:

std::mutex mutex;
std::condition_variable cv;

// 條件變量與臨界區(qū)有關(guān)牍鞠,用來(lái)獲取和釋放一個(gè)鎖,因此通常會(huì)和mutex聯(lián)用上沐。
std::unique_lock lock(mutex);
// 此處會(huì)釋放lock皮服,然后在cv上等待,直到其它線程通過(guò)cv.notify_xxx來(lái)喚醒當(dāng)前線程参咙,cv被喚醒后會(huì)再次對(duì)lock進(jìn)行上鎖,然后wait函數(shù)才會(huì)返回硫眯。
// wait返回后可以安全的使用mutex保護(hù)的臨界區(qū)內(nèi)的數(shù)據(jù)蕴侧。此時(shí)mutex仍為上鎖狀態(tài)
cv.wait(lock)

需要注意的一點(diǎn)是, wait有時(shí)會(huì)在沒(méi)有任何線程調(diào)用notify的情況下返回,這種情況就是有名的spurious wakeup两入。因此當(dāng)wait返回時(shí)净宵,你需要再次檢查wait的前置條件是否滿足,如果不滿足則需要再次wait裹纳。wait提供了重載的版本择葡,用于提供前置檢查。

template <typename Predicate>
void wait(unique_lock<mutex> &lock, Predicate pred) {
    while(!pred()) {
        wait(lock);
    }
}

除wait外, 條件變量還提供了wait_for和wait_until剃氧,這兩個(gè)名稱(chēng)是不是看著有點(diǎn)兒眼熟敏储,std::mutex也提供了_for和_until操作。在C++11多線程編程中朋鞍,需要等待一段時(shí)間的操作已添,一般情況下都會(huì)有xxx_for和xxx_until版本。前者用于等待指定時(shí)長(zhǎng)滥酥,后者用于等待到指定的時(shí)間更舞。

1.2 notify

了解了wait,notify就簡(jiǎn)單多了:?jiǎn)拘褀ait在該條件變量上的線程坎吻。notify有兩個(gè)版本:notify_one和notify_all缆蝉。

  • notify_one 喚醒等待的一個(gè)線程,注意只喚醒一個(gè)。
  • notify_all 喚醒所有等待的線程刊头。使用該函數(shù)時(shí)應(yīng)避免出現(xiàn)驚群效應(yīng)贝搁。

其使用方式見(jiàn)下例:

std::mutex mutex;
std::condition_variable cv;

std::unique_lock lock(mutex);
// 所有等待在cv變量上的線程都會(huì)被喚醒。但直到lock釋放了mutex芽偏,被喚醒的線程才會(huì)從wait返回雷逆。
cv.notify_all(lock)

2. 線程間通信 - chan的實(shí)現(xiàn)

有了上面的基礎(chǔ)我們就可以設(shè)計(jì)我們的線程間通訊工具"chan"了。我們的設(shè)計(jì)目標(biāo):

  • 在線程間安全的傳遞數(shù)據(jù)污尉。golang社區(qū)有一句經(jīng)典的話:不要通過(guò)共享內(nèi)存來(lái)通信膀哲,要通過(guò)通信來(lái)共享內(nèi)存。
  • 消除線程線程同步帶來(lái)的復(fù)雜性被碗。

我們先來(lái)看一下chan的實(shí)際使用效果, 生產(chǎn)者-消費(fèi)者(一個(gè)生產(chǎn)者某宪,多個(gè)消費(fèi)者)

#include <stdio.h>
#include <thread>
#include "chan.h"  // chan的頭文件

using namespace std::chrono;

// 消費(fèi)數(shù)據(jù) 
void consume(chan<int> ch, int thread_id) {
    int n;
    while(ch >> n) {
        printf("[%d] %d\n", thread_id, n);
        std::this_thread::sleep_for(milliseconds(100));
    }
}

int main() {
    chan<int> chInt(3);
    
    // 消費(fèi)者
    std::thread consumers[5];
    for (int i = 0; i < 5; i++) {
        consumers[i] = std::thread(consume, chInt, i+1);
    }

    // 生產(chǎn)數(shù)據(jù) 
    for (int i = 0; i < 16; i++) {
        chInt << i;
    }
    chInt.close();  // 數(shù)據(jù)生產(chǎn)完畢

    for (std::thread &thr: consumers) {
        thr.join();
    }

    return 0;
}

附: 源碼(可在github上下載到)

下面附上chan.simple.h的實(shí)現(xiàn),是chan的較為簡(jiǎn)單的實(shí)現(xiàn)锐朴,完整實(shí)現(xiàn)請(qǐng)去github下載兴喂。該代碼在g++和vc 2015下均編譯通過(guò),其它平臺(tái)未驗(yàn)證焚志。

// chan.simple.h
#pragma once
#include <condition_variable>  // std::condition_variable
#include <list>                // std::list
#include <mutex>               // std::mutex

template <typename T>
class chan {
    class queue_t {
        mutable std::mutex mutex_;
        std::condition_variable cv_;
        std::list<T> data_;
        const size_t capacity_;  // data_容量
        const bool enable_overflow_;
        bool closed_ = false;   // 隊(duì)列是否已關(guān)閉
        size_t pop_count_ = 0;  // 計(jì)數(shù)衣迷,累計(jì)pop的數(shù)量
    public:
        queue_t(size_t capacity) :
            capacity_(capacity == 0 ? 1 : capacity),
            enable_overflow_(capacity == 0) {
        }

        bool is_empty() const {
            return data_.empty();
        }
        size_t free_count() const {
            // capacity_為0時(shí),允許放入一個(gè)酱酬,但_queue會(huì)處于overflow狀態(tài)
            return capacity_ - data_.size();
        }
        bool is_overflow() const {
            return enable_overflow_ && data_.size() >= capacity_;
        }

        bool is_closed() const {
            std::unique_lock<std::mutex> lock(this->mutex_);
            return this->closed_;
        }

        // close以后的入chan操作會(huì)返回false, 而出chan則在隊(duì)列為空后才返回false
        void close() {
            std::unique_lock<std::mutex> lock(this->mutex_);
            this->closed_ = true;
            if (this->is_overflow()) {
                // 消除溢出
                this->data_.pop_back();
            }
            this->cv_.notify_all();
        }

        template <typename TR>
        bool pop(TR &data) {
            std::unique_lock<std::mutex> lock(this->mutex_);
            this->cv_.wait(lock, [&]() { return !is_empty() || closed_; });
            if (this->is_empty()) {
                return false;  // 已關(guān)閉
            }

            data = this->data_.front();
            this->data_.pop_front();
            this->pop_count_++;

            if (this->free_count() == 1) {
                // 說(shuō)明以前是full或溢出狀態(tài)
                this->cv_.notify_all();
            }

            return true;
        }

        template <typename TR>
        bool push(TR &&data) {
            std::unique_lock<std::mutex> lock(mutex_);
            cv_.wait(lock, [this]() { return free_count() > 0 || closed_; });
            if (closed_) {
                return false;
            }

            data_.push_back(std::forward<TR>(data));
            if (data_.size() == 1) {
                cv_.notify_all();
            }

            // 當(dāng)queue溢出,需等待queue回復(fù)正常
            if (is_overflow()) {
                const size_t old = this->pop_count_;
                cv_.wait(lock, [&]() { return old != pop_count_ || closed_; });
            }

            return !this->closed_;
        }
    };
    std::shared_ptr<queue_t> queue_;

public:
    explicit chan(size_t capacity = 0) {
        queue_ = std::make_shared<queue_t>(capacity);
    }

    // 支持拷貝
    chan(const chan &) = default;
    chan &operator=(const chan &) = default;
    // 支持move
    chan(chan &&) = default;
    chan &operator=(chan &&) = default;

    // 入chan壶谒,支持move語(yǔ)義
    template <typename TR>
    bool operator<<(TR &&data) {
        return queue_->push(std::forward<TR>(data));
    }

    // 出chan(支持兼容類(lèi)型的出chan)
    template <typename TR>
    bool operator>>(TR &data) {
        return queue_->pop(data);
    }

    // close以后的入chan操作返回false, 而出chan則在隊(duì)列為空后才返回false
    void close() {
        queue_->close();
    }

    bool is_closed() const {
        return queue_->is_closed();
    }
};
上一篇
C++11多線程-mutex(2)
目錄 下一篇
C++11多線程-promise
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市膳沽,隨后出現(xiàn)的幾起案子汗菜,更是在濱河造成了極大的恐慌,老刑警劉巖挑社,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陨界,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡痛阻,警方通過(guò)查閱死者的電腦和手機(jī)菌瘪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)录平,“玉大人麻车,你說(shuō)我怎么就攤上這事《氛猓” “怎么了动猬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)表箭。 經(jīng)常有香客問(wèn)我赁咙,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任彼水,我火速辦了婚禮崔拥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凤覆。我一直安慰自己链瓦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布盯桦。 她就那樣靜靜地躺著慈俯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拥峦。 梳的紋絲不亂的頭發(fā)上贴膘,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音略号,去河邊找鬼刑峡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛玄柠,可吹牛的內(nèi)容都是我干的突梦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼随闪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼阳似!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起铐伴,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俏讹,沒(méi)想到半個(gè)月后当宴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泽疆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年户矢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殉疼。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梯浪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瓢娜,到底是詐尸還是另有隱情挂洛,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布眠砾,位于F島的核電站虏劲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜柒巫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一励堡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧堡掏,春花似錦应结、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至游两,卻和暖如春砾层,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贱案。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工肛炮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宝踪。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓侨糟,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瘩燥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子秕重,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容