快捷搜索: 王者荣耀 脱发

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.使用锁层次
// 将应用程序分层,并且确认所有能够在任意给定的层级上被锁定的互斥元
// 当代码试图锁定一个互斥元时,如果它(当前执行的代码)在较低层已经持有锁定,
// 则不允许它锁定该互斥元。
// !!!!通过给每一个互斥元分配层号,并记录下每个线程都锁定了哪些互斥元,则可以在运行时
// 进行检查
经验分享 程序员 微信小程序 职场和发展