C++并发编程(3)——资源竞争下的安全栈

先上代码:

cpp 复制代码
#include <exception>
#include <memory>
#include <mutex>
#include <stack>

struct empty_stack : std::exception
{
    const char* what() const throw() {
        return "empty stack!";
    }
};

template<typename T>
class threadsafe_stack
{
private:
    std::stack<T> data;      // 实际存数据的栈
    mutable std::mutex m;    // 互斥量,用来保护 data

public:
    threadsafe_stack()
        : data(std::stack<T>()) {}

    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 new_value)
    {
        std::lock_guard<std::mutex> lock(m);
        data.push(new_value);
    }

    std::shared_ptr<T> pop()
    {
        std::lock_guard<std::mutex> lock(m);
        if (data.empty()) 
            throw empty_stack();       // 空栈则抛异常

        // 在修改栈之前,先构造好要返回的 shared_ptr
        std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
        data.pop();
        return res;
    }

    void pop(T& value)
    {
        std::lock_guard<std::mutex> lock(m);
        if (data.empty()) 
            throw empty_stack();

        value = data.top();
        data.pop();
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> lock(m);
        return data.empty();
    }
};

1. 自定义异常 empty_stack

cpp 复制代码
struct empty_stack : std::exception
{
    const char* what() const throw() {
        return "empty stack!";
    }
};
  • 继承自 std::exception,表示一种异常类型:栈为空时的异常

  • 重写了 what() 函数,返回错误信息 "empty stack!"

  • what() const throw()

    • const:保证这个函数不会修改对象状态。

    • throw():老式的异常说明,表示函数不会抛出异常(C++11 之后用 noexcept,这里是书上经典写法)。

以后当栈为空却调用 pop 时,就会 throw empty_stack(); 抛出这个异常。

2. 类模板 threadsafe_stack<T>

2.1 成员变量

cpp 复制代码
std::stack<T> data;
mutable std::mutex m;
  • data:真正存放元素的标准库栈。

  • m:互斥量,用来保护 data 的并发访问。

  • mutable

    • 允许在 const 成员函数中也能修改 m(加锁本身会修改互斥量的内部状态)。

    • 因为 empty() 被声明为 bool empty() const,但里面还要加锁,所以必须把 m 声明为 mutable

2.2 构造函数

cpp 复制代码
threadsafe_stack()
    : data(std::stack<T>()) {}
  • 默认构造一个空的 std::stack<T>

  • 写成初始化列表形式效率更好。

2.3 拷贝构造函数

cpp 复制代码
threadsafe_stack(const threadsafe_stack& other)
{
    std::lock_guard<std::mutex> lock(other.m);
    data = other.data;
}
  • 接受另一个 threadsafe_stack 的引用,做拷贝构造。

  • 首先 std::lock_guard<std::mutex> lock(other.m);

    • 构造时自动对 other.m 加锁;

    • 离开作用域时自动解锁(RAII机制)。

  • 在已经加锁的情况下安全地访问 other.data

    • data = other.data; 把对方的栈内容完整拷贝过来。
  • 这样就保证了:拷贝构造时不会与其他线程对 other 的并发访问产生数据竞争

2.4 删除拷贝赋值运算符

cpp 复制代码
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
  • 禁止赋值操作:a = b 这种用法不允许。

  • 原因:安全地实现一个"线程安全的赋值运算符"比较麻烦:

    • 要同时锁住 this->mother.m

    • 还要注意避免死锁;

    • 书中简单做法就是干脆不支持赋值,只支持拷贝构造。

  1. push:线程安全入栈
cpp 复制代码
void push(T new_value)
{
    std::lock_guard<std::mutex> lock(m);
    data.push(new_value);
}
  • 调用 push 时:

    • lock_guard 构造 → 对 m 加锁;

    • data.push(new_value); 安全地把元素压入栈;

    • 函数结束 → lock_guard 析构 → 自动解锁。

  • 这样多个线程同时 push 也不会互相冲突。

  1. 第一种 pop:返回 shared_ptr<T>
cpp 复制代码
std::shared_ptr<T> pop()
{
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) 
        throw empty_stack();

    std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
    data.pop();
    return res;
}

流程:

  1. 加锁:std::lock_guard<std::mutex> lock(m);

  2. 检查栈是否为空:

    • 如果为空,抛出 empty_stack 异常。
  3. 如果非空:

    • 先用 data.top() 的值构造一个 shared_ptr<T>

      cpp 复制代码
      std::make_shared<T>(data.top())
    • 然后再 data.pop(); 弹出栈顶。

    • 最后返回这个 shared_ptr<T>

这里有两个细节:

  1. 在修改栈之前先创建返回值

    如果你先 pop() 再去用那个值构造 shared_ptr,一旦中间抛异常(例如构造 T 时抛了),栈顶元素已经被移除,数据丢失。

    现在的写法是:

    • 先从 data.top() 复制一份内容出来构造 shared_ptr

    • 构造成功以后,再 data.pop() 修改栈;

    • 这样即使构造时抛异常,也还没修改栈内容,不会数据丢失。

  2. 返回 shared_ptr<T> 的好处

    • 避免返回引用或指针悬空的问题;

    • 即使栈内部已经把元素弹走,shared_ptr 还持有那份拷贝,外面用起来更安全。

  3. 第二种 pop:通过引用返回值

cpp 复制代码
void pop(T& value)
{
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) 
        throw empty_stack();

    value = data.top();
    data.pop();
}
  • 同样先加锁,再检查是否为空。

  • value = data.top(); 把栈顶元素复制到调用者提供的变量 value 中。

  • 然后 data.pop(); 弹出栈顶。

  • 这种写法比返回 shared_ptr<T> 少了一个堆分配(不用 new),适合拷贝成本不太高的类型。

注意:如果 T 的拷贝操作抛异常,那么在 pop() 中,data.pop() 不会执行,栈保持不变,这是异常安全的行为。

  1. empty():线程安全地检查是否为空
cpp 复制代码
bool empty() const
{
    std::lock_guard<std::mutex> lock(m);
    return data.empty();
}
  • 函数被声明为 const,说明逻辑上不修改对象状态。

  • 但内部依然需要加锁(会修改 m 的状态),所以互斥量必须是 mutable

  • 防止这样一种情况:一个线程刚检查完 empty()==false,另一个线程马上把最后一个元素 pop 掉,导致调用方以为还能 pop,但其实已经空了。所以正确模式一般是:
    不要先用 empty()pop(),而是直接在 pop() 内部检查并抛异常

    这里提供 empty() 主要是给一些非严格场景用。

关于锁的粒度的讨论:

① "之前对 top() 和 pop() 的讨论中,恶性条件竞争已经出现,因为锁的粒度太小,需要保护的操作并未全覆盖到。"

意思是:

  • top()pop() 是两个分开的操作

  • 如果每个操作分别加锁,很有可能一个线程执行完 top() 返回数据后,另一个线程在 pop() 之前就把数据弹走了

  • 这就产生了 条件竞争(race condition)

➡️ 原因:锁的粒度太小(只锁住某个操作,而不是整个逻辑动作)


② "不过,锁住的颗粒过大同样会有问题。"

这里说另一种极端情况:

  • 如果把所有操作都用一个大锁锁住

  • 虽然解决了 race condition

  • 但导致并发度极低,大家都在排队

➡️ 锁太大 → 性能下降


③ "一个全局互斥量要去保护全部共享数据...抵消了并发带来的性能提升。"

意思是:

  • 假设系统中所有共享数据都用一个互斥量来保护

  • 哪怕线程访问的不是同一块数据,也必须等待同一个锁

  • 最终导致并发几乎没效果

➡️ 全局大锁 = 只有一个线程能真正运行


④ "第一版为多处理器系统设计的 Linux 内核,用了一个全局内核锁。"

历史事实:

  • Linux 2.0 时代采用 Big Kernel Lock (BKL)

  • 内核中的大部分代码都必须先获取这个大锁才能运行

  • 导致多核 CPU 基本不能并行执行内核任务


⑤ "在双核系统上的性能比两个单核系统还差,四核更不能提了。"

原因:

  • 多个 CPU 同时想进入内核,却卡在那一个大锁上

  • 造成大量锁竞争、上下文切换(context switch)

  • CPU 态的并行度无法发挥出来

➡️ 多核系统本来应该更快

➡️ 但因为大锁的存在,反而更慢


⑥ "随后修正的 Linux 内核加入了细粒度锁方案......性能接近线性增长"

后来的 Linux 改进:

  • 将大锁拆解为多个小锁:

    • 多个子系统锁

    • 自旋锁

    • 读写锁

    • 每 CPU 锁

  • 让不同 CPU 访问不同资源时不会互相阻塞

  • 多核效率大幅提升

最终结果:

四核系统的性能 ≈ 单核性能 × 4

(接近线性扩展)

相关推荐
曼巴UE52 小时前
UE5 C++ 动态多播
java·开发语言
小小晓.2 小时前
Pinely Round 4 (Div. 1 + Div. 2)
c++·算法
SHOJYS2 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
热心市民蟹不肉3 小时前
黑盒漏洞扫描(三)
数据库·redis·安全·缓存
GIS数据转换器3 小时前
综合安防数智管理平台
大数据·网络·人工智能·安全·无人机
2501_915909063 小时前
iOS 反编译防护工具全景解析 从底层符号到资源层的多维安全体系
android·安全·ios·小程序·uni-app·iphone·webview
煤球王子3 小时前
学而时习之:C++中的异常处理2
c++
请一直在路上3 小时前
python文件打包成exe(虚拟环境打包,减少体积)
开发语言·python
luguocaoyuan3 小时前
JavaScript性能优化实战技术学习大纲
开发语言·javascript·性能优化