三、c++11智能指针之shared_ptr代码实现:线程安全

  • Post author:
  • Post category:其他




c++11智能指针之shared_ptr实现:线程安全

背景介绍:c++方向的硕士狗,实验室没什么c++项目,都是自学,在网上看到说智能指针这块挺重要的,自己也看了不少资料,觉得不如自己动手实战,从零到有实现一个shared_ptr。

规划:首先用c++11实现一个基本的智能指针,包括构造析构、拷贝移动等,然后往里面添加功能,如自定义删除器,支持数组管理的接口,以及目前我认为比较重要的多线程安全等等。最后采用c++17和20重构代码,看有没有什么需要改进的。

在代码实现中,我会一块附上如何调用这些接口,方便自己和大家学习


笔者也是刚入门c++,可能会有很多错误,欢迎大家指正,也欢迎大家一起学习,有任何问题可以留言评论



本节内容



本篇将会介绍在

系列文章一:基本功能



系列文章二:支持数组和常用接口

的基础上扩展线程安全。





一、线程安全

原来的基本框架:

template<typename T>
class mySharedSp {
private:
	int* m_count; //指向计数器的指针
	T* m_ptr; //多个智能指针对象共享一个引用计数,因此要定义为指针
	size_t* m_size; //数组大小

public:
	mySharedSp(T* p = NULL, size_t size = 0);  //构造
	mySharedSp(const mySharedSp<T>& other); //拷贝构造
	mySharedSp<T>& operator = (const mySharedSp<T>& other); //拷贝赋值
	mySharedSp(mySharedSp<T>&& other); //移动构造函数
	mySharedSp<T>& operator = (mySharedSp<T>&& other); //移动赋值
	~mySharedSp();

	//指针操作
	T& operator* () const;//解引用
	T* operator-> () const;//通过指针访问成员变量
	bool isNULL() const; //空指针检查
	int use_count() const; //获取引用值
	bool operator==(const mySharedSp<T>& other) const; //智能指针的比较操作
	bool operator!=(const mySharedSp<T>& other) const; //智能指针的比较操作

	// 迭代器
	using iterator = T*; // 将 iterator 定义为 T* 的别名
	using const_iterator = const T*; // 常量指针
	iterator begin(); // 常量对象调用
	iterator end();
	const_iterator begin() const; 
	const_iterator end() const;

	// 数组操作
	T& operator[](size_t index) const; //通过 ptr[index] 访问数组元素
	size_t size() const; //获取数组大小
	void resize(int newSize); //动态调整数组的大小
	void reserve(int newSize); //预留空间
	void sort(iterator begin = m_ptr, iterator end = m_ptr + *m_size, 
	bool (*comp)(const T&, const T&) = [](const T& a, const T& b) { return a < b; });  // 数组排序函数,使用lambda表达式,默认为升序排列
	T* find(const T& value) const; //查找
};



1. 互斥锁如何定义

这里我们将互斥锁定义为

mutex 的指针类型


  1. 为什么不是静态变量类型

    :考虑到一个智能指针类会被创建出多个实例,比如智能指针对象p1和p2指向资源1,p3和p4指向资源2。如果定义为静态变量,一个线程访问p1会阻塞p3,但他们本身就指向不同对象,本来是互不影响的,因此互斥锁不能定义为静态变量。

  2. 为什么不是 muex 类型

    :这样每个实例都有自己的互斥锁,若是智能指针对象p1和p2指向同一个共享资源1,他们的互斥锁不是同一个,则达不到线程安全的效果,因为互斥锁的本质可以理解有一个标志符,比如1表示可以获取,0表示不能获取。如果两个对象有两个自己的互斥锁,那怎么实现互斥锁内部这个计数器的同步呢,p1加锁了只改变p1自己的互斥锁,p2自己的互斥锁没变化,达不到线程同步的效果。
template<typename T>
class mySharedSp {
private:
	int* m_count; 
	T* m_ptr; 
	size_t* m_size; 
	mutex* m_mutex; // 互斥锁,定义为指针类型
public:
	...
};



2. 互斥锁保护什么


互斥锁保护的一定是共享资源。


由于在类的定义中,我们将 m_count,m_ptr,m_size,m_mutex 都定义为

指针类型

,并且每个智能指针对象都有一个自己的指针,但是他们指向的资源都是独一份的,即

*m_count,*m_ptr,*m_size,*m_mutex 都是共享资源

,因此我们使用 互斥锁

*m_mutex来保护 *m_count,*m_ptr,*m_size。


因此,只要出现更改 *m_count,*m_ptr,*m_size 的代码,都要使用互斥锁。

对于加锁解锁,我们创建了一个名为 lock 的

std::lock_guard 对象,并将 mutex 类型的 m_mutex 作为参数传递给它

,即

std::lock_guard<std::mutex> lock(m_mutex);

来对共享资源实现

自动管理互斥锁的加锁和解锁操作

。这样手动加锁后可以自动解锁(lock_guard 对象

离开其作用域时

,它会自动解锁互斥量),如下:

template<typename T>
void mySharedSp<T>::resize(int newSize) {
    std::lock_guard<std::mutex> lock(m_mutex); // 自动加锁
    ... // 其他代码
} // 自动解锁



3. 互斥锁如何使用

经过以上分析,当需要更改 *m_count,*m_ptr,*m_size 的时候,都要使用互斥锁。

实现shared共享指针,对 *m_count 的更改比较多,我们以此举例。

我们发现,当更改 *m_count 的时候,有以下两种情况:

  1. 计数器加一:调用拷贝构造,拷贝赋值等时候,需要 ++(*m_count) 。
  2. 计数器减一:调用移动赋值,析构等时候,需要 –(*m_count) ,并且判断当 *m_count = 0,销毁智能指针对象。

这里我们将这两种操作封装成类的函数,在其他函数中调用

template<typename T>
class mySharedSp {
private:
	int* m_count; //指向计数器的指针
	T* m_ptr; //多个智能指针对象共享一个引用计数,因此要定义为指针
	size_t* m_size; //数组大小
	std::mutex* m_mutex; // 互斥锁,定义为指针类型

public:
	...

private:
	void countAdd(); // 计数器加一
	void countDelete(); // 计数器减一,并判断是否销毁
};

类外定义:

// 计数器加一
template<typename T>
void mySharedSp<T>::countAdd() {
	std::lock_guard<std::mutex> lock(*m_mutex)
	++(*m_count);
}
// 计数器减一,并判断是否销毁
template<typename T>
void mySharedSp<T>::countDelete() {
	{
		lock_guard<mutex> lock(*m_mutex);
		--(*m_count); 
		bool ifDeleteMutex = false; // 是否销毁互斥锁的标志位,默认false
		if (*m_count == 0) {
			delete m_ptr;
			m_ptr = NULL;
			delete m_count;
			m_count = NULL;
			delete m_size;
			m_size = NULL;
			ifDeleteMutex = true; 
		}
	} // 局部作用域
	if (ifDeleteMutex) { //根据标志位最后再销毁,此时不能用 *m_count == 0 判断,因为 m_count 已经销毁了
		delete m_mutex;
		m_mutex = NULL;
	}
}


需要注意的是

,在 countDelete() 函数中,使用 lock_guard<mutex> lock(*m_mutex) 创建互斥锁,并在作用域外(通常情况下是函数结束时)解锁,如果不使用局部作用域,会出现一个问题,即在解锁前就已经 delete m_mutex 销毁了互斥锁,再解锁会出现问题。因此这里采用

花括号的局部变量,控制互斥锁在局部变量外进行解锁,然后在释放互斥锁。



4. 其他函数的重构



a. 构造

构造这里由于是新构造一个智能指针对象,因此也要新创建出一个互斥锁

//构造  
template<typename T>
mySharedSp<T>::mySharedSp(T* p, size_t size) : m_ptr(p), m_count(new int(1)), m_size(new int(size)), m_mutex(new mutex) {
	// m_mutex(new mutex) 创建一个互斥锁
	cout << "调用构造函数" << endl;
}



b. 拷贝构造

//拷贝构造
template<typename T>
mySharedSp<T>::mySharedSp(const mySharedSp<T>& other) : m_ptr(other.m_ptr), m_count(other.m_count), m_size(other.m_size), m_mutex(other.m_mutex) {
	countAdd(); // 计数器加一
	cout << "调用拷贝构造函数" << endl;
}



c. 拷贝赋值

++(*other.m_count); 是为了避免自赋值,这里也要用互斥锁保护

//拷贝赋值
template<typename T>
mySharedSp<T>& mySharedSp<T>::operator = (const mySharedSp<T>& other) {
	lock_guard<std::mutex> lock(*other.m_mutex); // 用互斥锁保护 *other.m_mutex
	++(*other.m_count);
	countDelete(); // 这里调用函数里面使用了互斥锁

	m_ptr = other.m_ptr;
	m_count = other.m_count;
	m_size = other.m_size;
	m_mutex = other.m_mutex;

	cout << "调用拷贝赋值函数" << endl;

	return *this;
}



d. 移动赋值

移动构造因为不涉及 *m_count,*m_size 的更改,因此代码跟原来一样。

移动赋值:

//移动赋值
template<typename T>
mySharedSp<T>& mySharedSp<T>::operator = (mySharedSp<T>&& other) {
	if (this != &other) {
		countDelete(); // 这里调用函数里面使用了互斥锁
		m_ptr = other.m_ptr;
		m_count = other.m_count;
		m_size = other.m_size;
		m_mutex = other.m_mutex;

		other.m_ptr = NULL; //不能delete,因为这时候 m_ptr 和 other.m_ptr 都指向同一块资源
		other.m_count = NULL;
		other.size = NULL;
		other.m_mutex = NULL;

		cout << "调用移动赋值函数" << endl;
	}
	return *this; 
}



e. 析构

//析构
template<typename T>
mySharedSp<T>::~mySharedSp() {
	if (m_ptr == NULL) {  
		cout << "指针为空,直接返回" << endl;
		delete m_count;
		m_count = NULL;
		delete m_size;
		m_size = NULL;
		delete m_mutex;
		m_mutex = NULL;
		return;
	}
	countDeleete(); // 这里调用函数里面使用了互斥锁
}



f. 其他接口

其他函数接口基本不变,但是 resize()有变化,因为更改了 *m_size 。

//调整数组大小
//如果是空指针,调用resize则创建新数组
template<typename T>
void mySharedSp<T>::resize(int newSize) {
	lock_guard<mutex> lock(*m_mutex); // 用来保护 *m_size 这个共享资源
	if (newSize < 0) {
		throw invalid_argument("Invalid argument");
		return;
	}
	T* newPtr = new T[newSize](); //使用了 值初始化 语法,初始化数组元素全为0
	if (m_ptr != NULL) { //原来的数组非空,则赋值
		size_t eleNumsToCopy = min{ newSize, *m_size };
		for (int i = 0; i < eleNumsToCopy; ++i) {
			newPtr[i] = m_ptr[i];
		}
		delete[] m_ptr; // 释放原指针
	}
	//若原本就是是空指针,resize则直接创建一个新的数组
	m_ptr = newPtr; // 将新的数组指针和大小同步给成员变量
	*m_size = newSize;
}



总结

本节是智能指针线程安全的问题,下一节会补充自定义删除器等函数接口。



版权声明:本文为Chiang11原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。