1. 程式人生 > >C++併發中的條件變數 std::condition_variable

C++併發中的條件變數 std::condition_variable

簡介

這個操作相當於作業系統中的Wait & Signal原語,程式中的執行緒根據實際情況,將自己阻塞或者喚醒其他阻塞的執行緒。

個人認為,條件變數的作用在於控制執行緒的阻塞和喚醒,這需要和鎖進行相互配合,用來實現併發程式的控制。

函式操作

wait和notify_one

void wait (unique_lock<mutex>& lck);

template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

相當於wait原語,lck是傳入的鎖,如果已經鎖定了,那麼當前執行緒(是指擁有lck鎖的那個執行緒)被阻塞,同時自動呼叫鎖的unlock()函式,允許其他執行緒進入臨界區;如果使用pred,那麼只有pred返回false時,進行阻塞。

void notify_one() noexcept;

喚醒一個被當前條件變數阻塞的執行緒,如果沒有阻塞的執行緒,那麼該函式沒有效果。

生產者消費者模型,該模型給出了一個最簡單的條件變數與臨界區配合的例子:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable> std::mutex mtx; std::condition_variable produce, consume; // 生產者和消費者共享的變數 int cargo = 0; void consumer() { std::unique_lock<std::mutex>lck(mtx); while(cargo == 0) { // 沒有貨物,消費者阻塞 consume.wait(lck); } std::cout << cargo <<
std::endl; // 表示一次消費 cargo = 0; produce.notify_one(); // 消費完畢後喚醒生產者 } void producer(int id) { std::unique_lock<std::mutex>lck(mtx); while(cargo != 0) { // 如果有貨物,生產者阻塞 produce.wait(lck); } cargo = id; // 生產一個貨物 consume.notify_one(); // 生產完畢後喚醒一個消費者 } int main() { std::thread consumers[10], producers[10]; // 產生生產者和消費者 for(int i = 0; i < 10; ++i) { consumers[i] = std::thread(consumer); producers[i] = std::thread(producer, i + 1); } // 等待所有執行緒執行完畢 for(int i = 0; i < 10; ++i) { producers[i].join(); consumers[i].join(); } return 0; } /* 輸出結果: 1 2 3 6 4 7 5 8 9 10 */

wait_for和wait_until

這兩個都是條件阻塞(等待)函式。

wait_for用於控制有時間限制的執行緒

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

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

rel_time內阻塞,如果超過這個時間就自動喚醒,或者是被notify類的函式喚醒。
程式碼例項:

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>

std::condition_variable cv;

int value;

void read_value() {
    std::cin >> value;
    cv.notify_one();
}

int main() {
    std::cout << "Please, enter an integer(I'll be printing dots)\n";
    std::thread th(read_value);

    std::mutex mtx;
    std::unique_lock<std::mutex>lck(mtx);
    // 在系統限制的時間內,一直等待
    while(cv.wait_for(lck, std::chrono::seconds(1)) == std::cv_status::timeout) {
        std::cout << "." << std::endl;
    }
    std::cout << "Yon entered: " << value << std::endl;
    th.join();
    return 0;
}

wait_until函式用於等待到指定的時間後自動喚醒或者被notify類喚醒:

template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,
                      const chrono::time_point<Clock,Duration>& abs_time);

template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,
                 const chrono::time_point<Clock,Duration>& abs_time,
                 Predicate pred);

同樣的,pred如果是false,就一直進行wait

notify_all

該函式一次性喚醒所有的阻塞執行緒,如果沒有阻塞執行緒,則函式沒有任何作用。
程式碼例項:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex>lck(mtx);
    while(!ready) {
        cv.wait(lck);
    }

    std::cout << "thread " << id << std::endl;
}

void go() {
    std::unique_lock<std::mutex>lck(mtx);
    ready = true;
    cv.notify_all();
}

int main() {
    std::thread threads[10];
    // spawn 10 threads
    for(int i = 0; i < 10; ++i) {
        threads[i] = std::thread(print_id, i);
    }
    std::cout << "10 threads ready to race...\n";
    go();

    for(auto& th : threads) {
        th.join();
    }
    return 0;
}
/*
輸出結果:(順序會亂)
10 threads ready to race...
thread 9
thread 6
thread 5
thread 2
thread 1
thread 0
thread 8
thread 4
thread 7
thread 3
*/

總結

如果一個執行緒對臨界區加鎖,那麼只要鎖定,其他執行緒就不能訪問該臨界區。而條件變數是對鎖進行操縱,可以這麼理解,每個鎖都屬於一個執行緒,對某個鎖進行wait或者notify大類的操作,相當於對當前擁有這個鎖的執行緒進行操作。

wait函式阻塞一個執行緒後,會對鎖進行unlock操作,很顯然,如果擁有鎖的執行緒阻塞了,而還不解鎖,那麼當前的臨界區會浪費掉。

每個條件變數可以對多個不同的鎖(可以理解為持有鎖的不同執行緒)進行wait或者notify類的操作。上面的生產者消費者模型中,使用兩個不同的條件變數,是為了更好的區分。