C++ 條件變量使用詳解

2021-03-02 CPP開發者

【導讀】:本文主要講解條件變量的詳細使用方法。

condition_variable介紹

在C++11中,我們可以使用條件變量(condition_variable)實現多個線程間的同步操作;當條件不滿足時,相關線程被一直阻塞,直到某種條件出現,這些線程才會被喚醒。

其主要成員函數如下:

條件變量是利用線程間共享的全局變量進行同步的一種機制,主要包括兩個動作:

另外一個線程使"條件成立",給出信號,從而喚醒被等待的線程。

為了防止競爭,條件變量的使用總是和一個互斥鎖結合在一起;通常情況下這個鎖是std::mutex,並且管理這個鎖 只能是 std::unique_lockstd::mutex RAII模板類。

上面提到的兩個步驟,分別是使用以下兩個方法實現:

等待條件成立使用的是condition_variable類成員wait 、wait_for 或 wait_until。

給出信號使用的是condition_variable類成員notify_one或者notify_all函數。

細節說明

在條件變量中只能使用std::unique_lock< std::mutex >說明

unique_lock和lock_guard都是管理鎖的輔助類工具,都是RAII風格;它們是在定義時獲得鎖,在析構時釋放鎖。它們的主要區別在於unique_lock鎖機制更加靈活,可以再需要的時候進行lock或者unlock調用,不非得是析構或者構造時。它們的區別可以通過成員函數就可以一目了然。在這裡插入圖片描述

wait/wait_for說明

線程的阻塞是通過成員函數wait()/wait_for()/wait_until()函數實現的。這裡主要說明前面兩個函數:

wait()成員函數

函數聲明如下:

void wait( std::unique_lock<std::mutex>& lock );
//Predicate 謂詞函數,可以普通函數或者lambda表達式
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

wait 導致當前線程阻塞直至條件變量被通知,或虛假喚醒發生,可選地循環直至滿足某謂詞。

wait_for()成員函數

函數聲明如下:

template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
                         const std::chrono::duration<Rep, Period>& rel_time);

template< class Rep, class Period, class Predicate >
bool wait_for( std::unique_lock<std::mutex>& lock,
               const std::chrono::duration<Rep, Period>& rel_time,
               Predicate pred);

wait_for 導致當前線程阻塞直至條件變量被通知,或虛假喚醒發生,或者超時返回。

返回值說明:

若經過 rel_time 所指定的關聯時限則為 std::cv_status::timeout ,否則為 std::cv_status::no_timeout 。

若經過 rel_time 時限後謂詞 pred 仍求值為 false 則為 false ,否則為 true 。

以上兩個類型的wait函數都在會阻塞時,自動釋放鎖權限,即調用unique_lock的成員函數unlock(),以便其他線程能有機會獲得鎖。這就是條件變量只能和unique_lock一起使用的原因,否則當前線程一直佔有鎖,線程被阻塞。

notify_all/notify_one

notify函數聲明如下:

void notify_one() noexcept;

若任何線程在 *this 上等待,則調用 notify_one 會解阻塞(喚醒)等待線程之一。

void notify_all() noexcept;

若任何線程在 *this 上等待,則解阻塞(喚醒)全部等待線程。

虛假喚醒

在正常情況下,wait類型函數返回時要不是因為被喚醒,要不是因為超時才返回,但是在實際中發現,因此作業系統的原因,wait類型在不滿足條件時,它也會返回,這就導致了虛假喚醒。因此,我們一般都是使用帶有謂詞參數的wait函數,因為這種(xxx, Predicate pred )類型的函數等價於:

while (!pred()) //while循環,解決了虛假喚醒的問題
{
    wait(lock);
}

原因說明如下:

假設系統不存在虛假喚醒的時,代碼形式如下:

if (不滿足xxx條件)
{
    //沒有虛假喚醒,wait函數可以一直等待,直到被喚醒或者超時,沒有問題。
    //但實際中卻存在虛假喚醒,導致假設不成立,wait不會繼續等待,跳出if語句,
    //提前執行其他代碼,流程異常
    wait();  
}

//其他代碼
...

正確的使用方式,使用while語句解決:

while (!(xxx條件) )
{
    //虛假喚醒發生,由於while循環,再次檢查條件是否滿足,
    //否則繼續等待,解決虛假喚醒
    wait();  
}
//其他代碼
....

條件變量使用

在這裡,我們使用條件變量,解決生產者-消費者問題,該問題主要描述如下:

生產者-消費者問題,也稱有限緩衝問題,是一個多進程/線程同步問題的經典案例。該問題描述了共享固定大小緩衝區的兩個進程/線程——即所謂的「生產者」和「消費者」,在實際運行時會發生的問題。

生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。

要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄數據),等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒,開始往緩衝區添加數據。

同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區添加數據之後,再喚醒消費者。

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

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> lk(g_cvMutex);
      //當隊列未滿時,繼續添加數據
      g_cv.wait(lk, [](){ 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;
      //喚醒其他線程 
      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> lk(g_cvMutex);
        //檢測條件是否達成
        g_cv.wait( lk,   []{ 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;
        //喚醒其他線程
        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;
}

運行結果:

相關焦點

  • C++ initializer_list 詳解
    無論是初始化對象還是某些時候為對象賦新值,都可以使用這樣一組由花括號括起來的初始值了。使用初始化列表時,可添加=,也可不添加。int units_sold=0;int units_sold(0);int units_sold={0}; int units_sold{0}; 當初始化列表用於內置類型的變量時,這種初始化形式有一個重要特點:如果我們使用列表初始化值存在丟失信息的風險,則編譯器將報錯:
  • C/C+編程筆記:const 變量詳解!一文了解具體用法
    2、全局const變量存放在只讀數據段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源文件使用(需要用extern關鍵字修飾) 在這裡解釋一下上述代碼:第二行代碼,使用了const(expression),顯示轉換,這是由於c++比c類型轉換更嚴格。
  • 走進C++11(三十)標準化條件變量 -- condition_variable
    std::condition_variable是條件變。Linux下使用 Pthread庫中的 pthread_cond_*() 函數提供了與條件變量相關的功能。和pthread_cond_*()一樣,我們可以使用條件變量(condition_variable)實現多個線程間的同步操作;當條件不滿足時,相關線程被一直阻塞,直到某種條件出現,這些線程才會被喚醒。
  • C++11線程、鎖和條件變量
    與lock_quard不同,它還支持延遲加鎖、時間鎖、遞歸鎖、鎖所有權的轉移並且還支持使用條件變量。這也是一個不可複製的類,但它是可以移動的類。_lock.unlock();  } 條件變量 C++11還提供了對另外一個同步原語的支持,這個原語就是條件變量。使用條件變量可以將一個或多個線程進入阻塞狀態,直到收到另外一個線程的通知,或者超時或者發生了虛假喚醒,才能退出阻塞狀態。
  • c++的輸入與輸出
    c++輸入與輸出C++ 標準庫提供了一組豐富的輸入/輸出功能,本章將討論 C++ 編程中最基本和最常見的 I/O 操作。輸入輸出並不是c++語言的正式組成成分,c和c++沒有為輸入輸出提供專門的結構。在c語言中輸入輸出是通過調用scanf和printf 實現的,在c++中是通過調用流對象cin和cout實現的。
  • C++、java 和 C 的區別
    一、基礎類型c++:** java:** C#:1.以java為準,c++裡面的int short long 像這樣的整型 一般都有unsigned 和signed的區分 ,這個跟java和c# 的區別比較大,但c#裡面有unit ulong ushort 這三種就相當於c++的修飾詞unsigned,當c++李明的變量類型定義unsigned,就默認是整數。
  • c++11新特性,所有知識點都在這了!
    auto & decltype關於C++11新特性,最先提到的肯定是類型推導,C++11引入了auto和decltype關鍵字,使用他們可以在編譯期就推導出變量或者表達式的類型,方便開發者編碼也簡化了代碼。
  • Python函數詳解一(函數參數、變量作用域)
    變量的作用域局部變量局部變量:在函數中定義的變量,只在函數內部起作用。如果想讓局部變量在函數外起作用,要聲明為全局變量,需要使用global關鍵字來聲明。全局變量全局變量:在模塊層次中定義的變量,作用範圍在整個模塊。1.全局變量的作用範圍僅限於單個模塊文件內。2.如果函數內定義的局部變量和全局變量同名,函數在使用該變量的時候會優先使用局部變量。
  • Linux Qt使用POSIX多線程條件變量、互斥鎖(量)
    比如說需要對線程間共享的數據提供保護,使用互斥量同步、使用條件變量、使用讀寫鎖同步等;各種同步方式用在什麼情況下,開始編程時多線程使用的並不多,無法切身體會到這些問題,後來程序寫的多了一點兒,慢慢接觸到一些多線程的東西,並且自己也可以學習了相關知識,並用到實際程序中。好了,下面以一個實際的例子為背景,來說明Linux POSIX多線程的一些特性。
  • C++ vector詳解
    以下是正文前言本文mark了vector的一些接口,介紹了vector中的對內存和對象的管理詳解請見cppreference-vector。1. vector內部管理著一塊內存,壓入對象的時候,會使用這塊內部的內存使用placement new去進行對象的生成,而釋放對象的時候,顯式的去調用析構函數去釋放對象。2. size代表vector中的實際含有元素數量,而capacity表示容量。
  • c++ 之布爾類型和引用的學習總結!
    a : 4 = 3;                       ^小結:1、c語言裡面的三目運算符返回的是變量值,它不能作為左值來使用。2、c++中的三目運算符可以直接返回變量本身,既可以作為右值使用,也可以作為左值來使用。3、c++中的三目運算符可能返回的值中如果有一個是常量值,則不能作為左值進行使用,這點要切記和理解。
  • C++ 的幾個for 循環,範圍for語句
    declaration部分負責定義一個變量,該變量將用於訪問序列中的基礎元素。每次迭代,declaration部分的變量會被初始化為expression部分的下一個元素值。例子:#include <iostream>using namespace std;int main(){ string str("this is a c++"); //每行輸出str中的一個字符 for(
  • 那些容易犯錯的c++保留字
    本文首發 | 公眾號:lunvey目前正在學習vc++6.0開發,而這裡面使用的是c++98標準。
  • c++簡介及順序結構
    注意:使用靜態類型的程式語言是在編譯時執行類型檢查,而不是在運行時執行類型檢查。2. 開發環境2.1. 介紹Dev-C++是一個Windows環境下的一個適合於初學者使用的輕量級 C/C++ 集成開發環境(IDE),實現對c++程序的編輯、編譯、運行和調試等工作。
  • 「最佳實踐」C++陷阱與套路
    ## 編譯器為什麼不給局部變量和成員變量做默認初始化因為效率,C++被設計為系統級的程式語言,效率是優先考慮的方向,c++秉持的一個設計哲學是「不為不必要的操作付出任何額外的代價」。所以它有別於java,不給成員變量和局部變量做默認初始化,如果需要賦初值,那就由程式設計師自己去保證。
  • 混合使用C、C++和彙編語之:內聯彙編和嵌入型彙編的使用
    使用它可以在C/C++程序中實現C/C++語言不能完成的一些工作。例如,在下面幾種情況中必須使用內聯彙編或嵌入型彙編。·程序中使用飽和算術運算(Saturatingarithmetic),如SSAT16和USAT16指令。·程序中需要對協處理器進行操作。
  • 一文帶你了解c++和c中字符串的使用
    有可能有些網友還沒怎麼接觸到c++(c++它是一門面向對象的語言,而c是一門面向過程的語言,所以這裡可能沒接觸過那個面向對象的網友不習慣這個用法,不過還是建議至少要掌握一門面向對象的語言,在這個發展快速的時代,不能太固步自封了(我這裡也是簡單的介紹一下c++中的字符串,不會設計到類和對象什麼的,只是和c語言做個對比)。)1、什麼是字符串?
  • C++基礎總結(一):從「hello world」入門C++!
    注意:使用靜態類型的程式語言是在編譯時執行類型檢查,而不是在運行時執行類型檢查。面向對象程序設計c++最大的亮點就是面向對象程序設計理念的運用。下面的表格列出了c++標準的發展歷史。讓我們看一段簡單的代碼,可以輸出單詞 Hello World。
  • C++ 的門門道道 | 技術頭條 - CSDN
    一、我的程序裡用了全局變量,但為什麼進程正常停止的時候會莫名其妙的core掉?Rule:C++在不同模塊(源文件)裡定義的全局變量,不保證構造順序;但保證在同一模塊(源文件)裡定義的全局變量,按定義的先後順序構造,按定義的相反次序析構。
  • python3使用ctypes在windows中訪問C和C++動態連結庫函數示例
    python3使用ctypes在windows中訪問C和C++動態連結庫函數示例這是我們的第一個示例,我們儘量簡單,不傳參,不返回,不訪問其他的動態連結庫一 測試環境介紹和準備Python3.7.0 (源碼和安裝文件)http://ffmpeg.club/python二 C/C++部分代碼1 首先完成C/C++的動態連結庫,與做python擴展庫不同,ctypes調用的c++