实现一个多线程安全的同步队列

0 阅读4分钟

遇到的一个笔试题

代码

//实现一个多线程安全的同步队列

#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <optional>

template <typename T>
struct SyncQueue
{
public:
    SyncQueue(size_t capacity) : capacity(capacity),
                                interrrupt_flag(false)
    {}
    SyncQueue() = delete;
    SyncQueue(const SyncQueue<T>&) = delete;  //禁拷贝因为包含禁拷贝的mutex、cv
    SyncQueue& operator=(const SyncQueue<T>&) = delete;
    ~SyncQueue()
    {
        interrupt();
    }

    //唤醒被阻塞的进程并退出
    void interrupt()
    {
        interrupt_flag.store(true);
        queue_not_full.notify_all();
        queue_not_empty.notify_all();
    }
    //尝试向队列中添加元素,如果失败立即返回
    bool try_push(const T& event)
    {
        std::lock_guard<std::mutex> lock(queue_mutex);
        if (queue.size() >= capacity || interrupt_flag.load())
        {
            return false;
        }
        queue.push(event);
        queue_not_empty.notify_one();
        return true;
    }
    //尝试从队列中取出一个元素,如果失败立即返回
    std::optional<T> try_pop()
    {
        std::lock_guard<std::mutex> lock(queue_mutex);
        if (queue.empty() || interrupt_flag.load())
        {
            return std::nullopt;
        }
        T element = queue.front();
        queue.pop();
        queue_not_full.notify_one();
        return element;
    }
    //向队列尾部添加元素,如果队列满则等待
    bool push(const T& element)
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        //等待队列不满或被中断
        queue_not_full.wait(lock, [this](){
            return queue.size() < capacity || interrupt_falg.load();
        })
        if (interrupt_flag.load())
        {
            return false;
        }
        queue.push(element);
        queue_not_empty.notify_one();
        return true;
    }
    //从队列取出并删除头部元素,如果队列空则等待
    std::optional<T> pop()
    {
        std::unique_lock<std::mutex> lcok(queue_mutex);
        //等待队列不空或被中断
        queue_not_empty.wait(lock, [this](){
            return !queue.empty() || interrupt_flag.load();
        })
        if (interrupt_flag.load())
        {
            return std::nullopt;
        }
        T element queue.front();
        queue_not_full.notify_one();
        return element;
    }

    size_t lenth()
    {
        std::lock_guard<std::mutex>(queue_mutex);
        return queue.size();
    }
    bool full()
    {
        std::lock_guard<std::mutex>(queue_mutex);
        return queue.size() >= capacity;
    }
    bool empty()
    {
        std::lock_guard<std::mutex> lock(queue_mutex);
        return queue.empty();
    }
    void clear()
    {
        std::lock_guard<std::mutex> lock(queue_mutex);
        while (!queue.empty())
        {
            queue.pop();
        }
        queue_not_full.notify_all();
    }

private:
    size_t capacity;
    std::atomic<bool> interrupt_flag;
    std::queue<T> queue;
    std::mutex queue_mutex;
    std::condition_variable queue_not_full;
    std::condition_variable queue_not_empty;
};

知识点

条件变量wait的原子三部曲

解锁->等待->重新加锁
wait(lock, 条件)是原子操作是原子操作,不会被打断,核心就是帮忙做了手动写会出错的 解锁+等待 绑定,以及唤醒后的 重新竞争锁+条件检查。

  1. 先释放锁:调用wait()瞬间,自动释放持有的unique_lock(否则其线程永远拿不到锁,队列卡死)
  2. 阻塞等待:线程挂起,直到收到notify_one/all的唤醒信号(或虚假唤醒)
  3. 重新竞争锁:被唤醒后,第一件事是和其他线程竞争互斥锁,抢到锁后才能继续执行,没抢到就继续阻塞在枪锁缓解

竞争锁成功后,必做条件二次校验(解决虚假唤醒/条件抢占)
抢到锁不代表就能执行业务逻辑,必须再判断队列是否真的满足条件(!empty()/!full()),原因有两个:

  • 虚假唤醒:操作系统无通知也会唤醒线程(POSIX标准允许),此时队列条件根本没满足;
  • 条件抢占:即使是正常notify唤醒,竞争锁过程中可能被其他同类型线程插队(比如2个Pop线程等待非空,notify后线程A先抢到锁并pop空队列,线程B抢到锁时队列又空了)

这就是为什么必须用while循环(或wait内嵌Lambda)--一次校验不够,要循环校验,直到条件真的满足。

总结执行流程:

queue_not_empty.wait(lock, [this](){
    return !q.empty();})  //Lambda版(编译器在底层帮忙实现了while循环)
//等价于手动while版
// while (q.empty()){queue_not_empty.wait(lock);}
  1. 线程执行到wait,先判断lambda,如果队列非空,直接跳过等待,执行后续Pop;
  2. 如果队列为空,原子释放锁+挂起等待;
  3. 其他线程push后调用notify_one,该线程被唤醒,开始和所有线程竞争锁;
  4. 抢到锁后,再次执行lambda,若队列非空,退出wait,执行pop;若队列为空(虚假唤醒/被插队),则再次原子释放锁+挂起等待,重复步骤2。

一个关键易错点:wait必须搭配std::unique_lock,不能是std::lock_guard
因为lock_guard是一次性加锁/解锁,不支持中途释放锁(wait需要中途解锁,唤醒后重新加锁)
而unique_lock支持灵活的锁状态切换,是条件变量的专属锁

Optional

Optional是C++17引入的空置安全容器,核心作用是显式表示一个值“存在/不存在”,替代传统的nullptr/空值判断,避免空指针解引用问题。在本多线程安全队列中,主要用来处理队列为空时pop返回“无值”的场景。
核心特点(本队列场景):

  1. 值语义:存储的是具体对象而非指针,无需手动管理内存,本队列中返回Optional,能直接返回元素副本/移动值,无内存泄漏风险。
  2. 显式空值:队列空时,返回std::nullopt(代表空值),调用方必须显式判断值是否存在,强制处理“空返回”的边界情况,符合多线程中“取数据可能失败”的逻辑。
  3. 安全访问:提供has_value()判断是否有值,value()获取值(无值时抛异常),value_or(默认值)无值时返回默认值,三种方式适配不同容错场景,不能直接解引用。
//调用方伪代码
if (auto opt_val = queue.pop(); opt_val.has_value()){
    T val = opt_val.value();//安全取值
    //处理数据
}else{
    //队列空的逻辑(如等待、重试)
}
    
或用value_or简化(无值时用默认值)
T val = queue.pop().value_or(T{});

和传统方式的对比(为什么比返回指针/布尔好)

方式问题
返回T*(指针)需手动new/delete,易内存泄漏
传引用+返回bool需提前创建对象,语义不直观
直接返回T队列空时无法合法返回值,会崩溃