参考学习:从零开始实现 C++ TinyWebServer 阻塞队列 BlockQueue类详解_c++阻塞队列-CSDN博客

原文讲的很详细,本文用于加强学习记忆。

 

阻塞队列

 阻塞队列是一种线程安全的数据结构,支持多线程环境中的生产者-消费者模型。其核心特点在于,当队列空时,消费者线程会进入阻塞状态,直到有新的数据可供消费;而当队列已满时,生产者线程会被阻塞,直至队列中有空闲空间可供使用。

  • 线程安全:借助同步机制,有效避免了多个线程同时操作队列时可能出现的数据竞争问题,确保数据的一致性和完整性。
  • 容量限制:可以根据实际需求灵活设置队列的容量上限,当队列达到最大容量时,生产者线程会被阻塞,避免数据溢出。
  • 阻塞操作:当队列为空时,消费者线程会自动等待;当队列满时,生产者线程也会进入等待状态,实现了线程间的协调与同步。

 

为什么需要阻塞队列

  • 解耦生产消费:将日志信息的产生和存储过程进行分离,应用程序只需将日志快速放入队列,无需等待写入操作完成,从而提高了系统的可维护性
  • 平衡速度差异:有效应对日志生产速度不稳定和消费速度受存储设备性能限制的问题,队列能够缓存多余的日志信息,实现动态平衡,确保系统的稳定运行。
  • 提升并发性能:支持多线程协作,生产者和消费者可以同时工作,充分利用多核处理器的性能优势,同时队列的同步机制避免了线程竞争,提高了系统的并发处理能力。
  • 保证日志安全:阻塞队列的先进先出特性确保日志按生产顺序进行存储,便于后续的问题排查和分析。

 

#ifndef BLOKQUEUE_H
#define BLOKQUEUE_H

#include <iostream>
#include <deque>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <assert.h>

template<class T>
class BlockQueue{
    public:
        BlockQueue(size_t max_size = 1000);
        ~BlockQueue();

        bool empty();
        bool full();
        void clear();
        size_t size();
        size_t capacity();

        void push_front(const T& item);
        void push_back(const T& item);
        bool pop(T& item);
        bool pop(T& item, int timeout);

        T front();
        T back();
        void flush();
        void close();

    private:
        bool is_close; //是否关闭
        size_t capacity_;   //容量
        std::deque<T> deque_; //双向队列
        
        std::mutex mtx_;  //
        std::condition_variable condition_producer; //生产者条件变量
        std::condition_variable condition_consumer;  //消费者条件变量
};

//模板的定义和实现要放在同一个头文件中
//因为模板的代码需要在编译时实例化

template<class T>
BlockQueue<T>::BlockQueue(size_t max_size):capacity_(max_size){
    assert(max_size > 0);
    is_close = false;
}

template<class T>
BlockQueue<T>::~BlockQueue(){
    close();
}

template<class T>
void BlockQueue<T>::push_back(const T& item){
    std::unique_lock<std::mutex> locker(mtx_);
    //队列满了,暂停生产
    while(deque_.size() >= capacity_){
        condition_producer.wait(locker); //防止虚假唤醒
    }
    deque_.push_back(item);
    condition_consumer.notify_one();

}

template<class T>
void BlockQueue<T>::push_front(const T& item){
    std::unique_lock<std::mutex> locker(mtx_);
    while(deque_.size() >= capacity_){
        condition_producer.wait(locker);
    }
    deque_.push_front(item);
    condition_consumer.notify_one();
}

template<class T>
bool BlockQueue<T>::pop(T& item){
    std::unique_lock<std::mutex> locker(mtx_);
    //队列空了,暂停消费
    while(deque_.empty()){
        if(is_close){return false;}
        condition_consumer.wait(locker);
    }
    item = deque_.front();
    deque_.pop_front();
    condition_producer.notify_one();
    return true;
}

template<class T>
bool BlockQueue<T>::pop(T& item, int timeout){
    std::unique_lock<std::mutex> locker(mtx_);
    const std::cv_status TIMEOUT_STATUS = std::cv_status::timeout;
    while(deque_.empty()){
        if(is_close){return false;}
        if(condition_consumer.wait_for(locker, std::chrono::seconds(timeout)) == TIMEOUT_STATUS){
            return false;
        }
    }
    item = deque_.front();
    deque_.pop_front();
    condition_producer.notify_one();
    return true;
}


//关闭阻塞队列, 唤醒所以生产者和消费者
template<class T>
void BlockQueue<T>::close(){
    {
        std::lock_guard<std::mutex> locker(mtx_);
        deque_.clear();
        is_close = true;
    }
    condition_producer.notify_all();
    condition_consumer.notify_all();
}

//唤醒消费者
template<class T>
void BlockQueue<T>::flush(){
    condition_consumer.notify_one();
}

template<class T>
T BlockQueue<T>::front(){
    std::lock_guard<std::mutex> locker(mtx_);
    return deque_.front();
}

template<class T>
T BlockQueue<T>::back(){
    std::lock_guard<std::mutex> locker(mtx_);
    return deque_.back();
}

template<class T>
bool BlockQueue<T>::empty(){
    std::lock_guard<std::mutex> locker(mtx_);
    return deque_.empty();
}

template<class T>
bool BlockQueue<T>::full(){
    std::lock_guard<std::mutex> locker(mtx_);
    return deque_.size() >= capacity_;
}

template<class T>
void BlockQueue<T>::clear(){
    std::lock_guard<std::mutex> locker(mtx_);
    deque_.clear();
}

template<class T>
size_t BlockQueue<T>::size(){
    std::lock_guard<std::mutex> locker(mtx_);
    return deque_.size();
}

template<class T>
size_t BlockQueue<T>::capacity(){
    std::lock_guard<std::mutex> locker(mtx_);
    return capacity_;
}

#endif 

来剖析一下代码:

私有成员变量

  • bool is_close
    • 作用:标识队列是否已关闭
    • 初始值:false,表示队列未关闭
    • 用途:在pop操作中,如果is_close为true,则返回false,表示队列已关闭,不在允许移除元素。
  • size_t capacity_;
    • 作用:存储队列的最大容量
    • 初始值:在构造函数中设置,例如1000
    • 用途:用于限制队列中元素的最大数量,在push_back和push_front操作中,如果队列已满,则生成者线程会等待。
  • std::deque<T> deque_
    • 作用:存储队列中的元素
    • 类型: 双端队列(std::deque),允许在队列的两端高效地添加和移除元素。
    • 用途:用于存储和管理队列中的元素。
  • std::mutex mtx_
    • 作用:互斥量,用于保护队列的访问
    • 类型: std::mutex
    • 用途:在多线程环境中,确保对队列的操作是线程安全的。通过锁定互斥量mtx_,可以防止多个线程同时修改队列的状态,从而避免数据竞争和不一致的状态。
  • std::condition_variable condition_producer
    • 作用:生产者条件变量,用于通知生产者线程。
    • 类型:std::condition_variable
    • 用途:当队列中有空闲空间时,生产者线程可以被唤醒,继续添加元素。在pop操作中,如果队列中元素被移除,生产者条件变量会被通知,唤醒一个等待的生产者线程。

    

构造函数:

//构造函数:初始化容量,默认最大1000个元素
template<class T>
BlockQueue<T>::BlockQueue(size_t max_size):capacity_(max_size){
    assert(max_size > 0);   //容量必须大于0
    is_close = false;   //初始化状态为"未关闭"
}

使用assert作为判定,将is_close设置为false,初始化队列状态为"未关闭";

关闭队列:close()

 1 // 关闭队列:清空数据,唤醒所有等待线程,标记为“已关闭”
 2 template<class T>
 3 void BlockQueue<T>::close(){
 4     {    // 局部作用域:只在修改状态时加锁
 5     std::lock_guard<std::mutex> locker(mtx_);   // 获取锁
 6     deque_.clear(); //清空队列中的数据
 7     is_close = true;     // 标记为关闭(后续入队操作会间接失效)
 8     }
 9      // 唤醒所有等待的生产者和消费者(别让它们一直等)
10     condition_producer.notify_all();//唤醒所有生产者线程
11     condition_consumer.notify_all();//唤醒所有消费者线程
12 }

使用局部作用域,在作用域中创建变量之类的只有在局部作用域中生效,除了作用域就自动销毁;在局部作用域中去获取锁,清空队列中的数据,再将is_close设置为true停止队列的服务。这个函数就是直接停止服务了,出了局部作用域后自动销毁锁;唤醒所有线程;这里使用的上锁方式时lock_guard上锁方式,查了一下说这种上锁方式通常使用于构造函数上锁,在析构函数自动进行释放,也是在作用域内进行作用,贯穿生命周期,使用这种上锁方式相对比较简单;下面还有一种上锁方式unique_lock的上锁方式.

析构函数

1 //析构函数:关闭队列,释放资源
2 template<class T>
3 BlockQueue<T>::~BlockQueue(){
4     close();    //确保队列关闭时的资源清理
5 }

析构函数直接调用close函数,清除队列历史数据,关闭队列服务,唤醒所有线程。

push_back

 1 template<class T>
 2 void BlockQueue<T>::push_back(const T& item){//将数据添加队尾
 3     std::unique_lock<std::mutex> locker(mtx_);  //加锁(自动释放,避免死锁)
 4     //队列满了。暂停生产
 5     //用while而非if:防止”虚假唤醒“ (条件变量可能在未满足条件时被唤醒)
 6     while(deque_.size() >= capacity_){
 7         condition_producer.wait(locker);    //    释放锁并阻塞,被唤醒时重新获取锁  
 8     }
 9     deque_.push_back(item); //入队
10     condition_consumer.notify_one();    //通知一个等待的消费者(有新数据了)
11 
12 }

将数据插入队尾;这里使用unique_lock上锁方式进行上锁,这种上锁方式优点就是使用灵活,方式比较多,上锁后在下方使用condition_producer.wait可以临时释放锁,阻塞在这个位置,等待别人的唤醒.

在这里就是通过判断双端队列的长度是否大于设置的最大容量进行临时释放锁,阻塞线程.  当队列可以放置数据的时候就退出循环阻塞,进行队尾入队操作,再唤醒一个等待的消费者去取得数据消费.

push_front

1 template<class T>
2 void BlockQueue<T>::push_front(const T& item){//将数据加入队头
3     std::unique_lock<std::mutex>  locker(mtx_);  //加锁(自动释放,避免死锁)
4     while(deque_.size() >= capacity_){
5         condition_producer.wait(locker);    // 队列满时阻塞
6     }
7     deque_.push_front(item);    // 队头入队
8     condition_consumer.notify_one();     // 通知消费者
9 }

这里是从队头插入,与上方的差别不大.

pop

 1 // 阻塞出队:队列空时一直等,直到有数据或队列关闭
 2 template<class T>
 3 bool BlockQueue<T>::pop(T& item){
 4     std::unique_lock<std::mutex> locker(mtx_);
 5     //队列空了,暂停消费
 6      // 队列空时,阻塞消费者(等待生产者入队)
 7     while(deque_.empty()){
 8         if(is_close){return false;}// 队列已关闭且空,返回失败
 9         condition_consumer.wait(locker);     // 释放锁并阻塞
10     }
11     item = deque_.front();  // 获取队头数据
12     deque_.pop_front(); // 出队
13     condition_producer.notify_one();    // 通知一个等待的生产者(队列有空间了)
14     return true;
15 }

出队操作,同样先上锁,因为是出队操作,所以要确保队列不为空,所以使用判断队列是否为空作为循环条件,同时还有对队列是否关闭作为中间的判断,如果有线程提前设置了队列关闭就会直接返回false否则临时释放锁并阻塞线程.  如果队列不为空,就准备进行出队操作,从队头中取出,这里传入的是指针可以指向数据的地址,从队列中取出一个数据意味着队列中有空间,就唤醒一个阻塞的生产者线程进行生产.   返回true反馈操作正常

 

超时出队pop

 1 // 超时出队:队列空时最多等timeout秒,超时返回失败
 2 template<class T>
 3 bool BlockQueue<T>::pop(T& item, int timeout){
 4     std::unique_lock<std::mutex> locker(mtx_);
 5     const std::cv_status TIMEOUT_STATUS = std::cv_status::timeout;
 6     while(deque_.empty()){
 7         if(is_close){return false;}  // 队列已关闭且空,返回失败
 8         if(condition_consumer.wait_for(locker, std::chrono::seconds(timeout)) == TIMEOUT_STATUS){
 9             return false;
10         }
11     }
12     item = deque_.front();
13     deque_.pop_front();
14     condition_producer.notify_one();     // 通知生产者
15     return true;
16 }

使用重载的方式,重新书写出队函数,设置超时机制。

std::cv_status是 C++ 标准库(定义在 <condition_variable> 头文件中)的一个枚举类型,专门用于表示条件变量等待操作的结果状态。这里使用const表明变量TIMEOUT_STATUS为一个常量,这里使用const的作用就是防止修改,遵循最小权限原则:编程中的 “最小权限原则” 指:给变量或函数赋予完成任务所必需的最小权限,避免不必要的修改权限。还是依据队列是否为空循环处理,但是中间还设置了超时机制:
1.condition_consumer.wait_for(...),这是条件变量的超时等待函数,功能是:
  • 释放锁并阻塞:暂时释放 locker 持有的 mtx_ 锁(允许其他线程操作队列),当前线程进入 “等待状态”,最多等待 timeout 秒。
  • 返回值:
    • std::cv_status::timeout:表示等待超时(timeout 秒内没有被唤醒)。
    • std::cv_status::no_timeout:表示等待未超时(在 timeout 秒内被其他线程通过 notify_one()/notify_all() 唤醒)。

    

2.std::chrono::seconds(timeout),这是 C++11 引入的时间单位包装器,用于指定等待的时间长度:

  • std::chrono::seconds(timeout) 表示 “timeout 秒”。
  • 例如,若 timeout = 5,则表示等待 5 秒。

  说的是chrono是一个时间库,使用这种方法相当于一种指定时间类型为秒级,是一种时间间隔类型

if(condition_consumer.wait_for(locker, std::chrono::seconds(timeout)) == TIMEOUT_STATUS){
            return false;
        }

这就相当于让线程释放locker这个锁,阻塞timeout秒,如果在时限内没唤醒就是超时了,函数返回false表示出队失败。

 

flush()  唤醒一个等待的消费者
// 唤醒一个等待的消费者(强制检查队列)
template<class T>
void BlockQueue<T>::flush(){
    condition_consumer.notify_one();
}

 

front()  仅读取队头元素
1 template<class T>
2 T BlockQueue<T>::front(){
3     std::lock_guard<std::mutex> locker(mtx_);
4     return deque_.front();
5 }

上锁,防止被其他线程干扰读取的数据,确保这个读取的数据无误。这里使用的就是lock_guard方式上锁,操作简单,只要上锁了就行,等到生命周期结束自动释放锁。

 

back()  读取队尾元素
1 template<class T>
2 T BlockQueue<T>::back(){
3     std::lock_guard<std::mutex> locker(mtx_);
4     return deque_.back();
5 }

这个和上面读取队头元素原理差不多

 

empty()  // 队列是否为空(加锁检查)
1 template<class T>
2 bool BlockQueue<T>::empty(){    // 队列是否为空(加锁检查)
3     std::lock_guard<std::mutex> locker(mtx_);
4     return deque_.empty();
5 }

 

full()  // 队列是否满(加锁检查)
1 template<class T>
2 bool BlockQueue<T>::full(){     // 队列是否满(加锁检查)
3     std::lock_guard<std::mutex> locker(mtx_);
4     return deque_.size() >= capacity_;
5 }

 

clear()  // 清空队列(加锁操作)
1 template<class T>
2 void BlockQueue<T>::clear(){     // 清空队列(加锁操作)
3     std::lock_guard<std::mutex> locker(mtx_);
4     deque_.clear();
5 }

 

size()  // 当前元素数量(加锁获取)
1 template<class T>
2 size_t BlockQueue<T>::size(){    // 当前元素数量(加锁获取)
3     std::lock_guard<std::mutex> locker(mtx_);
4     return deque_.size();
5 }

 

capacity()  // 最大容量(加锁获取)
1 template<class T>
2 size_t BlockQueue<T>::capacity(){       // 最大容量(加锁获取)
3     std::lock_guard<std::mutex> locker(mtx_);
4     return capacity_;
5 }

 

 

 

在原文中还有对于BlockQueue的测试代码,这里就不进行分解了。