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

互斥量(std::mutex)是多線程間同時訪問某一共享變量時,保證變量可被安全訪問的手段。在多線程編程中,還有另一種十分常見的行爲:線程同步線程同步是指線程間需要按照預定的先後次序順序進行的行爲。C++11對這種行爲也提供了有力的支持,這就是條件變量。條件變量位於頭文件condition_variable。條件變量能阻塞一個或多個線程,直到收到另外一個線程發出的通知或者超時,纔會喚醒當前阻塞的線程。條件變量需要和互斥量配合起來用。

  • condition_variable,配合std::unique_lock < std::mutex>進行wait操作。
  • condition_variable_any,和任意帶有lock、unlock語義的mutex搭配使用,比較靈活,但效率比condition_variable差一些。

condition_variable是一個類,搭配互斥量mutex來用,這個類有它自己的一些函數,這裏就主要講wait函數和notify_*函數,故名思意,wait就是有一個等待的作用,notify就是有一個通知的作用簡而言之就是程序運行到wait函數的時候會先在此阻塞,然後自動unlock,那麼其他線程在拿到鎖以後就會往下運行,當運行到notify_one()函數的時候,就會喚醒wait函數,然後自動lock並繼續下運行。

wait

wait是線程的等待動作,直到其它線程將其喚醒後,纔會繼續往下執行。

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

// 條件變量與臨界區有關,用來獲取和釋放一個鎖,因此通常會和mutex聯用。
std::unique_lock lock(mutex);
// 此處會釋放lock,然後在cv上等待,直到其它線程通過cv.notify_xxx來喚醒當前線程,
// cv被喚醒後會再次對lock進行上鎖,然後wait函數纔會返回。
// wait返回後可以安全的使用mutex保護的臨界區內的數據。此時mutex仍爲上鎖狀態
cv.wait(lock)

需要注意的一點是, wait有時會在沒有任何線程調用notify的情況下返回,這種情況就是有名的虛假喚醒。因此當wait返回時,你需要再次檢查wait的前置條件是否滿足,如果不滿足則需要再次wait。wait提供了重載的版本,用於提供前置檢查。

wait還有第二個參數,這個參數接收一個布爾類型的值,當這個布爾類型的值爲false的時候線程就會被阻塞在這裏,只有當該線程被喚醒之後,且第二參數爲true纔會往下運行。

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

除wait外, 條件變量還提供了wait_for和wait_until,這兩個名稱是不是看着有點兒眼熟,std::mutex也提供了_for和_until操作。在C++11多線程編程中,需要等待一段時間的操作,一般情況下都會有xxx_for和xxx_until版本。前者用於等待指定時長,後者用於等待到指定的時間。

notify

瞭解了wait,notify就簡單多了:喚醒wait在該條件變量上的線程。notify有兩個版本:notify_one和notify_all。

  • notify_one 喚醒等待的一個線程,注意只喚醒一個。
  • notify_all 喚醒所有等待的線程。使用該函數時應避免出現驚羣效應(多個線程等待一個喚醒的情況叫做驚羣效應)。
std::mutex mutex;
std::condition_variable cv;

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

notify_one()每次只能喚醒一個線程,那麼notify_all()函數的作用就是可以喚醒所有的線程但是最終能搶奪鎖的只有一個線程,或者說有多個線程在wait,但是用notify_one()去喚醒其中一個線程,那麼這些線程就出現了去爭奪互斥量的一個情況,那麼最終沒有獲得鎖的控制權的線程就會再次回到阻塞的狀態,那麼對於這些沒有搶到控制權的這個過程就叫做虛假喚醒那麼對於虛假喚醒的解決方法就是加一個while循環,比如下面這樣:

while (que.size() == 0) {
    cr.wait(lck);
}

這個就是當線程被喚醒以後,先進行判斷,是否可以去操作,如果可以再去運行下面的代碼,否則繼續在循環內執行wait函數。

條件變量使用

在這裏,我們使用條件變量,解決生產者-消費者問題,該問題主要描述如下:
生產者-消費者問題,也稱有限緩衝問題,是一個多進程/線程同步問題的經典案例。該問題描述了共享固定大小緩衝區的兩個進程/線程——即所謂的“生產者”和“消費者”,在實際運行時會發生的問題。
生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據
要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄數據),等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒,開始往緩衝區添加數據。同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區添加數據之後,再喚醒消費者。

生產者-消費者代碼如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <windows.h>
#include <condition_variable>

std::mutex g_cvMutex;// 全局互斥鎖
std::condition_variable g_cv; // 全局條件變量

std::deque<int> g_data_deque;//緩衝區,全局消息隊列
const int  MAX_NUM = 30;//緩存區最大數目
int g_next_index = 0;//數據

//生產者,消費者線程個數
const int PRODUCER_THREAD_NUM  = 3;
const int CONSUMER_THREAD_NUM = 3;

void producer_thread(int thread_id)
{
    while (true)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::unique_lock <std::mutex> lock(g_cvMutex);//加鎖
        //當隊列未滿時,繼續添加數據
        g_cv.wait(lock, [](){ return g_data_deque.size() <= MAX_NUM; });
        g_next_index++;
        g_data_deque.push_back(g_next_index);
        std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
        std::cout << " queue size: " << g_data_deque.size() << std::endl;

        // 通知前,手動解鎖以防正在等待的線程被喚醒後又立即被阻塞。
        lock.unlock();
        g_cv.notify_all();//喚醒其他線程
    }
}

void consumer_thread(int thread_id)
{
    while (true)
    {
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(550));
            std::unique_lock<std::mutex> lock(g_cvMutex);//加鎖
            g_cv.wait(lock, [] { return !g_data_deque.empty(); });//檢測條件是否達成

            //互斥操作,消息數據
            int data = g_data_deque.front();
            g_data_deque.pop_front();
            std::cout << "\tconsumer_thread: " << thread_id << " consumer data: ";
            std::cout << data << " deque size: " << g_data_deque.size() << std::endl;

            // 這裏用大括號括起來了 爲了避免出現虛假喚醒的情況 所以先unlock 再去喚醒
        }
        g_cv.notify_all(); //喚醒其他線程
    }
}

int main()
{
    std::thread arrRroducerThread[PRODUCER_THREAD_NUM];
    std::thread arrConsumerThread[CONSUMER_THREAD_NUM];

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
    {
        arrRroducerThread[i] = std::thread(producer_thread, i);
    }

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
    {
        arrConsumerThread[i] = std::thread(consumer_thread, i);
    }

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
    {
        arrRroducerThread[i].join();
    }

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
    {
        arrConsumerThread[i].join();
    }
    return 0;
}
/*輸出
producer_thread: 2 producer data: 1 queue size: 1
producer_thread: 0 producer data: 2 queue size: 2
producer_thread: 1 producer data: 3 queue size: 3
        consumer_thread: 0 consumer data: 1 deque size: 2
        consumer_thread: 1 consumer data: 2 deque size: 1
        consumer_thread: 2 consumer data: 3 deque size: 0
producer_thread: 2 producer data: 4 queue size: 1
...
*/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章