C++并发编程学习03:一个简易的线程安全栈实现
线程安全栈
学习内容来自于“C++并发编程实战”
#pragma once #include <exception> #include <memory> // std::make_shared 和 智能指针 #include <mutex> // lock_guard #include <stack> struct empty_stack : std::exception { const char* what() const throw(); }; template<typename T> class ThreadSafe_Stack { private: std::stack<T> m_data; mutable std::mutex m_mutex; public: ThreadSafe_Stack() { } ThreadSafe_Stack(const ThreadSafe_Stack& other) { std::lock_guard<std::mutex> lock(other.m_mutex); m_data = other.m_data; // 在构造函数体中执行复制 } // 栈本身不能被赋值 ThreadSafe_Stack& operator=(const ThreadSafe_Stack&) = delete; void push(T new_value) { std::lock_guard<std::mutex> lock(m_mutex); m_data.push(new_value); } // 通过使用std::shared_ptr<>使得允许栈来处理内存分配问题,避免了对new和delete的过多调用 std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m_mutex); if (m_data.empty()) throw empty_stack(); // 执行pop的时候判断栈是否为空 //make_shared 在动态内存中分配一个对象并初始化,返回指向改对象的shared_ptr std::shared_ptr<T> const res(std::make_shared<T>(m_data.top())); m_data.pop(); return res; } void pop(T& value) { std::lock_guard<std::mutex> lock(m_mutex); if (m_data.empty()) throw empty_stack(); value = m_data.top(); // 用标准库栈stack?? m_data.pop(); } bool empty() const { std::lock_guard<std::mutex> lock(m_mutex); return m_data.empty(); } }; // 锁粒度可能会造成的问题 // 1.单个的全局互斥元保护所有共享的数据 // 在一个有大量共享数据的系统中,可能会消除并发所有的性能优势,因为线程被限制为每次只能运行一个 // 即使它们访问了同一个数据的不同部分 // 多处理系统的Linux内核的第一个版本,使用了单个全局内核锁,导致:一个双处理器系统 // 通常比两个单处理器系统的性能更差 // Linux内核的后续版本:转移到一个更细粒度的锁定方案,四个处理器的性能更接近于理想的 // 单处理器系统的4倍 // 死锁的解决 // 1.避免嵌套锁:如果已经持有一个锁,就不再获取锁 // 互斥元锁定是思索最常见的诱因 // 2.持有锁的时候,避免调用用户提供的代码 // 自己在持有锁的时候,若调用用户提供的代码,因为用户提供的代码可能会获取锁 // 则可能出现嵌套锁的情况 // 3.以固定的顺序获取锁 // 如果需要获取两个或者更多的锁,并且不能以std::lock的单个操作取得 // 次优的做法:在每个线程中以相同的顺序获取它们(避免成环)。 // 对链表数据加锁的保护(也会造成死锁:可以定义遍历的顺序) // 1.为链表上的每一个节点都设置一个互斥元 // 2.为访问这个链表,线程必须获取它们感兴趣的每个节点上的锁 // 3.若某个线程需要删除某个节点: 必须获取待删除的节点及其两边的节点共三个节点的锁。 // 4.遍历时,线程在获取序列中下一个节点上的锁的时候,必须保持当前节点上的锁 // 以确保指向下一节点的指针在次期间不会被修改,一旦获取到下一节点上的锁,则可以释放前面节点上的锁 // 这种方式也可能会导致死锁 // 4.使用锁层次 // 将应用程序分层,并且确认所有能够在任意给定的层级上被锁定的互斥元 // 当代码试图锁定一个互斥元时,如果它(当前执行的代码)在较低层已经持有锁定, // 则不允许它锁定该互斥元。 // !!!!通过给每一个互斥元分配层号,并记录下每个线程都锁定了哪些互斥元,则可以在运行时 // 进行检查