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 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 

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类的操作。上面的生产者消费者模型中,使用两个不同的条件变量,是为了更好的区分。

你可能感兴趣的:(C++笔记)