遇到的一个笔试题
代码
//实现一个多线程安全的同步队列
#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, 条件)是原子操作是原子操作,不会被打断,核心就是帮忙做了手动写会出错的 解锁+等待 绑定,以及唤醒后的 重新竞争锁+条件检查。
- 先释放锁:调用
wait()瞬间,自动释放持有的unique_lock(否则其线程永远拿不到锁,队列卡死) - 阻塞等待:线程挂起,直到收到
notify_one/all的唤醒信号(或虚假唤醒) - 重新竞争锁:被唤醒后,第一件事是和其他线程竞争互斥锁,抢到锁后才能继续执行,没抢到就继续阻塞在枪锁缓解
竞争锁成功后,必做条件二次校验(解决虚假唤醒/条件抢占)
抢到锁不代表就能执行业务逻辑,必须再判断队列是否真的满足条件(!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);}
- 线程执行到
wait,先判断lambda,如果队列非空,直接跳过等待,执行后续Pop; - 如果队列为空,原子释放锁+挂起等待;
- 其他线程push后调用
notify_one,该线程被唤醒,开始和所有线程竞争锁; - 抢到锁后,再次执行lambda,若队列非空,退出
wait,执行pop;若队列为空(虚假唤醒/被插队),则再次原子释放锁+挂起等待,重复步骤2。
一个关键易错点:wait必须搭配std::unique_lock,不能是std::lock_guard
因为lock_guard是一次性加锁/解锁,不支持中途释放锁(wait需要中途解锁,唤醒后重新加锁)
而unique_lock支持灵活的锁状态切换,是条件变量的专属锁
Optional
Optional是C++17引入的空置安全容器,核心作用是显式表示一个值“存在/不存在”,替代传统的nullptr/空值判断,避免空指针解引用问题。在本多线程安全队列中,主要用来处理队列为空时pop返回“无值”的场景。
核心特点(本队列场景):
- 值语义:存储的是具体对象而非指针,无需手动管理内存,本队列中返回Optional,能直接返回元素副本/移动值,无内存泄漏风险。
- 显式空值:队列空时,返回
std::nullopt(代表空值),调用方必须显式判断值是否存在,强制处理“空返回”的边界情况,符合多线程中“取数据可能失败”的逻辑。 - 安全访问:提供
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 | 队列空时无法合法返回值,会崩溃 |