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類的操作。上面的生產者消費者模型中,使用兩個不同的條件變數,是為了更好的區分。