1. 程式人生 > >C++併發實戰11:條件變數

C++併發實戰11:條件變數

1  執行緒睡眠函式

std::this_thread::sleep_for(std::chrono::milliseconds(100));//標頭檔案#include<chrono>,供選擇的如seconds()等
       不要使用睡眠函式同步執行緒,睡眠函式可以用於復現執行緒的一些行為。

    執行緒重新排程函式:

std::this_thread::yield()
重新排程本執行緒,用於執行緒等待其它執行緒時而不阻塞本執行緒

2  條件變數std::condition_variable不允許拷貝和移動的。其基本語義和Linux的pthread_cond_t差不多。

condition_variable();
condition_variable (const condition_variable&) = delete;//沒有copy constructor和move constructor

~condition_variable();//阻塞在此條件變數上的執行緒將被喚醒,不會有執行緒再對此條件變數wait
void wait (unique_lock<mutex>& lck);//等待條件發生,注意引數是unique_lock,可能發生虛假喚醒,即不是notify喚醒的

template <class Predicate>
  void wait (unique_lock<mutex>& lck, Predicate pred);//pred是個函式物件返回bool,執行緒會在pred返回false的下阻塞,pred返回true會被喚醒,這樣可以防止虛假喚醒等價於:while (!pred()) wait(lck);
template <class Rep, class Period>

  cv_status wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time);//在指定rel_time時間段內等待,此期間可能被notify喚醒
template <class Rep, class Period, class Predicate>
       bool wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred);//為了防止虛假喚醒,加了一個pred,在rel_time內被notify且要pred返回true方可被喚醒

template <class Clock, class Duration>
  cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time);//等待到一個指定的絕對時間點,語義和wait_for差不多
template <class Clock, class Duration, class Predicate>
       bool wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time,Predicate pred);

void notify_one() noexcept;//若有多個執行緒等待此條件變數則選擇一個執行緒喚醒,若沒有執行緒等待則此函式什麼也不做
void notify_all() noexcept;//喚醒所有阻塞在此條件變數上的執行緒,若沒有阻塞執行緒此函式什麼也不做

3  下面是一個使用condition_variable和queue實現的單生產者單消費者程式碼片段
std::mutex mut;//該mutex會被condition_variable使用
std::queue<data_chunk> data_queue;//用於在生產者執行緒和消費者執行緒間傳遞資料
std::condition_variable data_cond;
void data_preparation_thread()//生產者
{
   while(more_data_to_prepare())
   {
      data_chunk const data=prepare_data();
      std::lock_guard<std::mutex> lk(mut);//這裡可以用lock_guard或者unique_lock
      data_queue.push(data);// 
      data_cond.notify_one();//喚醒一個阻塞在條件變數的執行緒,多個阻塞執行緒會選擇一個喚醒,若沒有阻塞的執行緒則什麼也不做
   }
}
void data_processing_thread()//消費者
{
   while(true)//不斷消費
   {
      std::unique_lock<std::mutex> lk(mut);//注意condition_varibale必須使用unique_lock,因為unique_lock提供了lock和unlock操作更加靈活 
      data_cond.wait(lk,[]{return !data_queue.empty();});//##1##lambda表示式用於檢測queue是否為空,這裡可以是任意的callable object
      data_chunk data=data_queue.front();
      data_queue.pop();
      lk.unlock();//wait返回時mutex處於locked,為了提高併發應該立即顯示解鎖
      process(data);//處理資料
      if(is_last_chunk(data))
         break;
   }
}
        ##1##處說明:

        wait首先檢查lambda表示是否為真(queue是否為空),若為假(queue中有資料)wait直接返回;若為假則表明條件不滿足,那麼wait會將mutex解鎖,並使執行緒進入阻塞狀態,這裡看出為什麼要用unique_lock了吧因為其提供了比lock_guard更靈活的lock和unlock操作。

        當阻塞的執行緒被notify_one()/notify_all()喚醒時,首先對mutex上鎖,並檢查lambda表示式(queue是否為空的條件)。若條件滿足(queue非空)則wait立即返回,mutex處於locked。若條件仍不滿足(queue為空),wait將對mutex解鎖,並繼續阻塞執行緒在waiting狀態。

4 測試wait前後mutex的狀態

#include<iostream>
#include<thread>
#include<mutex>
#include<chrono>
#include<condition_variable>
using namespace std;
mutex m;
condition_variable cond;
int flag=0;
void producer(){
    this_thread::sleep_for(chrono::seconds(1));
    lock_guard<mutex> guard(m);
    flag=100;
    cond.notify_one();
    cout<<"notify..."<<endl;
}
void customer(){
    unique_lock<mutex> lk(m);
    if(m.try_lock())
        cout<<"mutex unlocked after unique_lock"<<endl;
    else
        cout<<"mutex locked after unique_lock"<<endl;//輸出
    while(flag==0){
        cout<<"wait..."<<endl;
        cond.wait(lk);
    }
    if(m.try_lock())
        cout<<"mutex unlocked after wait"<<endl;
    else
        cout<<"mutex locked after wait"<<endl;//輸出
    cout<<"flag==100? "<<flag<<endl;
}
int main(){
    thread one(producer);
    thread two(customer);
    one.join();
    two.join();
    return 0;
}

程式輸出:

mutex locked after unique_lock        //unique_lock(mutex&)會擁有mutex並對mutex上鎖
wait...
notify...
mutex locked after wait       //wait返回後mutex仍處於locked狀態
flag==100? 100