#include <iostream>
#include <mutex>
#include <memory>
#include <utility>
#include <stack>
template<class T>
class threadsafe_stack {
private:
std::stack<T> data;
mutable std::mutex m;
public:
threadsafe_stack(){}
threadsafe_stack(const threadsafe_stack& other) {
std::lock_guard<std::mutex> lock(other.m);
data = other.data;
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T val) {
std::lock_guard<std::mutex> lock(m);
data.push(std::move(val));
}
std::shared_ptr<T> pop() {
std::lock_guard<std::mutex> lock(m);
if (data.empty()) {
return nullptr;
}
std::shared_ptr<T> const res(std::make_shared<T>(std::move(data.top())));
data.pop();
return res;
}
bool pop(T& val) {
std::lock_guard<std::mutex> lock(m);
if (data.empty()) {
return false;
}
val = std::move(data.top());
data.pop();
return true;
}
bool empty() const {
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};
一、互斥视角下:出栈返回智能指针的核心原因
1. 核心目标:保证互斥锁保护范围内的 “异常安全”
互斥锁的核心是保证 “共享资源操作的原子性”,但仅加锁还不够 —— 需确保锁作用域内的操作即使抛异常,共享资源(栈)的状态也不会被破坏,智能指针是实现这一目标的最优解:
(1)问题根源:直接返回值的异常风险(破坏原子性)
若在互斥锁内直接返回值,流程如下:
// 危险设计:锁内直接返回值
T pop() {
std::lock_guard<std::mutex> lock(m); // 加锁(保证原子性)
T temp = data.top(); // 步骤1:拷贝栈顶值(可能抛异常)
data.pop(); // 步骤2:修改共享资源(出栈)
return temp; // 步骤3:返回值(可能抛异常)
}
- 风险:步骤 1/3 若因内存不足、自定义类型拷贝构造抛异常,步骤 2 已执行(栈顶元素被移除),但值未传递给调用者,共享资源状态与数据不一致,互斥锁的原子性保障失去意义;
- 本质:修改共享资源(
data.pop())的操作,不能放在 “可能抛异常的拷贝 / 返回” 之前。
(2)智能指针的解决方案:先安全拷贝,再修改共享资源
std::shared_ptr<T> pop() {
std::lock_guard<std::mutex> lock(m); // 加锁(原子性基础)
if (data.empty()) return nullptr; // 空判断(下文详解)
// 步骤1:堆上创建智能指针(拷贝/移动值,若抛异常,栈未修改)
std::shared_ptr<T> const res(std::make_shared<T>(std::move(data.top())));
// 步骤2:仅当拷贝成功,才修改共享资源(原子性+异常安全)
data.pop();
return res;
}
- 核心逻辑:互斥锁保证 “判空→创建智能指针→出栈” 全程原子化,而智能指针将 “拷贝值” 提前到 “修改栈” 之前 —— 即使拷贝抛异常,栈的状态不变,彻底避免数据丢失;
- 额外优势:智能指针自动管理堆内存,无需手动释放,避免裸指针在多线程下的悬空 / 泄漏问题。
2. 互斥锁与智能指针的配合原则
- 互斥锁保证 “操作流程的原子性”(不被其他线程打断);
- 智能指针保证 “原子流程内的异常安全”(流程不被异常打断);
- 二者结合,才是真正的 “线程安全 + 异常安全”。
二、互斥视角下:空判断的核心规则
1. 空判断必须在互斥锁内(原子性的关键)
(1)错误做法:锁外判空,破坏原子性
// 致命错误:判空和出栈拆分为两个锁区域
if (!stack.empty()) { // 锁1:判空后解锁,间隙出现
stack.pop(); // 锁2:出栈前加锁,可能已空
}
- 问题:两次加锁之间有 “解锁→其他线程入 / 出栈→再加锁” 的间隙,即使互斥锁本身是安全的,组合操作也失去原子性,可能导致空栈出栈崩溃;
- 本质:互斥锁的原子性必须覆盖 “判空 + 出栈” 的完整业务逻辑,而非单个操作。
(2)正确做法:锁内判空,保证原子性
std::shared_ptr<T> pop() {
std::lock_guard<std::mutex> lock(m); // 加锁,原子化区域开始
if (data.empty()) return nullptr; // 锁内判空,无间隙
// 出栈逻辑...
}
bool pop(T& val) {
std::lock_guard<std::mutex> lock(m); // 加锁,原子化区域开始
if (data.empty()) return false; // 锁内判空,无间隙
// 出栈逻辑...
}
- 核心:互斥锁从函数开始就持有,“判空→出栈” 全程在锁内,其他线程无法在中间插入操作,保证组合逻辑的原子性;
- 空判断的返回值(
nullptr/false):替代抛异常,让调用者无需捕获异常,同时不破坏锁内原子性。
2. 空判断的额外作用:异常安全兜底
- 锁内判空是 “最后一道防线”:即使调用者违规先调用
empty()再调用pop(),锁内的空判断仍能避免空栈出栈的非法操作; - 判空后直接返回:不执行后续的
data.top()/data.pop(),避免访问空栈的未定义行为,保证共享资源操作的合法性。
核心总结
- 返回智能指针的核心:在互斥锁保证原子性的基础上,解决 “拷贝 / 返回抛异常导致共享资源状态破坏” 的问题,是原子性 + 异常安全的双重保障;
- 锁内空判断的核心:保证 “判空 + 出栈” 的完整逻辑在互斥锁作用域内,消除间隙,避免原子性失效,同时兜底空栈操作的合法性;
- 互斥锁的关键:不仅要保护单个操作,更要保护 “检查 + 操作” 的完整业务逻辑,智能指针和锁内判空都是这一原则的具体体现。
773



被折叠的 条评论
为什么被折叠?



