C++多线程编程:其七、条件变量的使用

一、条件变量的作用

一句话:如果一个线程的执行,必须要等待另一个线程的执行结果,就需要采用这种异步等待机制。
考虑一个场景:有两个线程,线程A负责写一个缓冲区,线程B负责读取缓冲区。怎么才能保证读写顺序呢?
(1)线程B轮询,看到有数据就处理。显然不好。会无端浪费CPU。
(2)使用条件变量,持有资源的线程唤醒等待资源的线程。
(3)使用future和aysnc,下一篇会详细介绍。

二、条件变量的使用接口

在这里插入图片描述
解释一下wait(),有两个重载版本:

void wait(std::unique_lock<std::mutex>& lock);
//Predicate 谓词函数,可以普通函数或者lambda表达式
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

(1)wait(std::unique_lockstd::mutex& lock)函数的作用,线程持有这把锁互斥锁的时候调用该函数,那么当前线程就会放弃对互斥锁的控制权。争抢这把互斥锁的某一个线程会得到这把锁。之后该线程会进入阻塞状态,阻塞状态不会占用CPU资源。当其他线程执行唤醒函数时,阻塞状态的线程会进入运行状态。
(2)void wait(std::unique_lockstd::mutex& lock, Predicate pred)函数。Predicate是谓词函数,通常是函数对象。只有当 pred 条件为 false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。

三、条件变量实现生产者、消费者模型

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

std::deque<int> q;
std::mutex mu;
std::condition_variable cond;

void product() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);
        std::cout << "product put a value: " << count << std::endl;
        locker.unlock();
        cond.notify_one();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        count--;
    }
}

void consume() {
    int data = 0;
    while ( data != 1) {
        std::unique_lock<std::mutex> locker(mu);
        cond.wait(locker,[](){
            return !q.empty();
        });
        data = q.back();
        q.pop_back();
        std::cout << "consume got a value: " << data << std::endl;
    }
}
int main() {
    std::thread t1(product);
    std::thread t2(consume);
    t1.join();
    t2.join();
    return 0;
}

生产者、消费者模型的关键点:
(1)生产者和消费者不能同时访问消息队列;
(2)当生产者获得消息队列访问权时,不考虑队列长度的情况下,可以不限制的先消息队列写入数据;
(3)当消费者获得消息队列访问权时,如果消息队列为空,则会进入阻塞状态,等待消息队列有数据。当生产者写入数据后,会通知消费者可以读数据。

对此分析一下上面代码的行为:
(1)t1是生产者线程,t2是消费者线程;二者通过互斥锁保障线程安全,某个时刻消息队列只能被一个线程占有。
(2)如果消费者线程占有了互斥锁,则会通过:

cond.wait(locker,[](){
     return !q.empty();
});

判读是否应该阻塞。如果队列中有数据,则谓词函数返回true,执行wait不会阻塞。继续执行后面的取数据逻辑。如果队列中没有数据,则谓词函数返回false,wait函数阻塞,并放弃对互斥锁的控制权。如果生产者线程此前争抢互斥锁失败进入阻塞状态,此刻会获得互斥锁进入运行状态。

(3)如果生产者线程占有了互斥锁,则会立刻向消息队列里面放数据。之后会手动解锁,再后面执行notify_one()会唤醒被阻塞的进程。
注意三点:
第一点,必须是先解锁再唤醒消费者线程,否则消费者线程被唤醒后仍然是阻塞状态,无法抢占互斥锁;
第二点,生产者线程执行完notify_one()不会进入阻塞状态,仍然可以抢占互斥锁,理论上可能出现这种情况:生产者一次性将数据写完,之后线程终止,再之后消费者线程才开始读取数据。
第三点,消费者线程收到唤醒以后,会根据谓词函数判断下一步操作。如果队列不为空,谓词函数返回true,阻塞解除,消费者线程可以去争抢互斥锁;如果队列仍然为空,则继续阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值