生成指定长度队列_互联网大厂面经:写一个bug-free的生产消费队列

更多互联网新鲜资讯、工作奇淫技巧关注原创【飞鱼在浪屿】(日更新)


背景介绍

本文假设你对多线程的互斥量/mutex和条件变量/condition variables有所了解,并且用过C++。这里将用现代C++并发写一个无bug的生产消费队列。

mutex一般用在保护共享资源和数据上,同时使多个线程完成其工作。

有时还需要及时同步它们之间的操作,那么使用指示任务是否已完成的标记变量。比如希望线程等到某个条件为真,然后再允许它继续执行。那这个可以用condition variables实现。


为什么我们需要condition variables

想象一下不使用条件变量怎么解决问题。

线程可以循环,不断不断地检查互斥量,直到线程拿到了可以进入关键区代码的互斥。然后结束检查,继续执行。

此方法有几个问题:

  1. 效率很低,因为循环让 Cpu 保持忙绿
  2. mutex意味着在一个时间点只有一个线程可以进行操作。
  3. 其他线程将没有可用资源得到执行

标准库提供了等待发生特定事件的机制 。一个变量与特定事件关联,线程可以让这个变量等待事件发生。在此等待期间,线程已睡眠,即不占用任何 CPU时间片。当事件实际发生时,能够唤醒等待的线程!


条件变量

条件变量可用于阻止(条件变量本身不占用任何资源)一个或多个线程,直到其他线程修改共享变量的状态(受mutex锁保护),然后通知一个或多个阻塞的线程状态已经更改,唤起阻塞的线程。、
条件变量与 配合使用,这意味着您需要同时使用 两者来实现所需的行为。打算对共享数据进行操作的所有线程必须遵循以下流程顺序执行:

  1. 获取,在用于保护共享变量的同一个mutex,获取std::unique_lock<:mutex>
  2. 执行 、或在条件变量上执行 。等待操作(等待本身是原子调用,wait,wait_for,wait_until)挂起暂停线程的执行,等待兼具释放mutex的语义。
  3. 当条件变量被通知、超时过期或发生虚假唤醒时,线程将唤醒,并且以原子方式重新获取mutex。然后,线程应检查条件,如果唤醒是虚假的(后面有说),则继续等待。

生成消费队列模型

为了展示它的用法,假设您有一个固定长度的S队列和两组线程:

  1. 生成者,仅在队列尚未满时。放入数据到队列
  2. 使用者,队列存在数据时,从队列中弹出元素。

正如你了解是,生产者可能需要停止工作并等待队列具有一些可用空间,而使用者则可能需要停止,等待队列数据不为空。每当生产者推送到队列中时,它可以通知任何等待的使用者该队列不再为空。同样,每当使用者成功从队列中弹出一个元素时,它可以通知任何生产者该队列不再已满。

我们将编写一个线程安全的类std::queuesafe_queue,来包装上述行为。类包含以下两种方法

  1. enqueue()
  2. dequeue()

和四个私有字段:

  1. max_size是队列的最大长度。
  2. mutable std::mutex mtx。mutable的作用是让const函数也可以修改mutex。需要mutex是因为在读取队列的时候,其他线程可能同时写入队列。即使我们没有真正修改队列,也可能会修改到队列长度size。
  3. condition_variable
  4. std::queue

以下是线程安全队列的整个代码

template struct safe_queue {  safe_queue(const size_t _max_size)      : max_size(_max_size), mtx(), condition(), queue() {}   size_t empty() const {    std::unique_lock<:mutex> l(mtx);    return queue.empty();  }   size_t size() const {    std::unique_lock<:mutex> l(mtx);    return queue.size();  }  void enqueue(T t) {    std::unique_lock<:mutex> l(mtx);     while (queue.size() >= max_size) {      condition.wait(l);    }     queue.push(t);    l.unlock();    condition.notify_all();  }   T dequeue() {    std::unique_lock<:mutex> l(mtx);     while (queue.empty() ) {      condition.wait(l);    }     T val = queue.front();    queue.pop();     l.unlock();    condition.notify_all();    return val;  }  private:  uint max_size;  mutable std::mutex mtx;  std::condition_variable condition;  std::queue queue;};

代码分析

我们应该注意到的第一件事是,现在我们使用std::unique_lock<:mutex> 作为锁。 (即std::lock_guard)。 std::unique_lock构造的时候加锁, 销毁的时候解锁。这种操作也称为RAII机制


enqueue入队操作

该方法由生产者使用,它非常简单:生产者首先获取mutex(我们需要确保在其他线程读取或写入队列时不修改队列),然后等待,直到队列有空间加入一个元素。此处要记住的关键部分是,当条件变量将线程置于睡眠状态时,将释放队列(condition.wait(l);), 以便其他线程可以在队列上操作。另一个重要事实是,每当线程被唤醒时,它都会确保拥有锁。

wait()也可能被虚假地唤醒线程,这意味着当条件未满足时,线程可以唤醒,即使没有线程发出条件变量信号,线程也可能从等待状态唤醒。这就是为什么以下代码会 嵌入在 while 循环中的原因:

while (queue.size() >= max_size) {      condition.wait(l);}

每当线程被唤醒时,它自动获得锁,因此我们确信没有别的线程正在使用队列,可以安全地检查队列是否具有空间容纳新元素,如果没有,则意味着已被虚假通知唤醒,我们再次进入睡眠状态。

最后,当队列有空间使用新元素时,我们可以继续执行下一个指令(我们目前仍然拥有锁),即push一个元素到队列,然后释放锁(此时我们完成了关键部分),并通知可能wait在条件变量上的所有其他线程。

queue.push(t);l.unlock();condition.notify_all();return;

dequeue出队操作

和入队工作方式类似,即首先获取锁,检查我们是否具有先决条件,以便能够从队列中弹出一个元素,如果没有先决条件,使用条件变量进入睡眠状态。
一旦被条件变量环境,我们自然拥有锁,我们可以继续在队列中操作,从而最终弹出一个元素,并通知所有其他线程,队列中数据有变化更新。

还有另一种wait的方式,预测一个判断签名是真的,才会结束wait。

template< class Predicate >void wait( std::unique_lock<:mutex>& lock, Predicate pred );

Predicate返回真或假,一般写成 lambda 函数)这样写等同于一开始的写法

while (!pred()) {    wait(lock);}

然后我们可以重写函数enqueue()和dequeue()如下

void enqueue(T t) {    std::unique_lock<:mutex> l(mtx);     condition.wait(l ,      [this](){        return queue.size() < max_size;      });       queue.push(t);    l.unlock();    condition.notify_all();  }     T dequeue() {    std::unique_lock<:mutex> l(mtx);         condition.wait(l ,      [this](){        return !queue.empty;      });         T val = queue.front();    queue.pop();     l.unlock();    condition.notify_all();     return val;  }

这里没有了一开始的显式循环写法,减轻了检查虚假唤醒的负担。每当从这个wait返回,我们确信这是真的。


a4c08beddb4877529a9e94960cb126ba.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值