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 的指针类型
。
为什么不是静态变量类型
:考虑到一个智能指针类会被创建出多个实例,比如智能指针对象p1和p2指向资源1,p3和p4指向资源2。如果定义为静态变量,一个线程访问p1会阻塞p3,但他们本身就指向不同对象,本来是互不影响的,因此互斥锁不能定义为静态变量。
为什么不是 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 的时候,有以下两种情况:
- 计数器加一:调用拷贝构造,拷贝赋值等时候,需要 ++(*m_count) 。
- 计数器减一:调用移动赋值,析构等时候,需要 –(*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;
}
总结
本节是智能指针线程安全的问题,下一节会补充自定义删除器等函数接口。