shared_ptr的简易实现

实现一个简易shared_ptr模板类需要些什么

  1. 引用计数
  2. 控制对引用计数访问权限的互斥锁
  3. 构造函数,拷贝构造函数,重载赋值运算符,解引用运算符等等
  4. 析构函数,避免在临界区访问引用计数

类的私有成员变量

1
2
3
4
5
6
7
template<class T> 
class my_shared_ptr{
private:
int* _p_ref_count;
std::mutex* _p_mutex;
T* _p_ptr;
};

定义构造与析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
template<class T>
class my_shared_ptr{
public:
my_shared_ptr(T* ptr=nullptr):
_p_ref_count(new int(1)),
_p_mutex(new std::mutex),
_p_ptr(ptr) {}
my_shared_ptr(const my_shared_ptr<T>& msp)
_p_ref_count(msp._p_ref_count),
_p_mutex(msp._p_mutex),
_p_ptr(msp._p_ptr) {
add_ref_count();
}
~my_shared_ptr() {
release();
}
my_shared_ptr<T>& operator=(const my_shared_ptr<T>& msp) {
if(_p_ptr!=msp._p_ptr) {
release(); // 释放旧资源
_p_ref_count = msp._p_ref_count;
_p_mutex = msp._p_mutex;
_p_ptr = msp._p_ptr;
add_ref_count();
}
return *this;
}
T& operator*() {
return *_p_ptr;
}
T* operator->() {
return _p_ptr;
}
T* get() {
return _p_ptr;
}
int get_ref_count() {
return *_p_ref_count;
}
void add_ref_count() {
_p_mutex->lock();
++(*_p_ref_count);
_p_mutex->unlock();
}
private:
void release() {
bool delete_flag = false;
_p_mutex->lock();
if(--(*_p_ref_count)==0) {
delete _p_ref_count;
delete _p_ptr;
delete_flag = true;
}
_p_mutex->unlock();
if(delete_flag) delete _p_mutex;
}
};

线程安全问题

  1. my_shared_ptr对象中的引用计数因为使用了互斥锁所以是线程安全的
  2. 但是my_shared_ptr管理的对象存放在堆上,如果两个线程同时访问,则将造成线程安全问题

存在的问题

my_shared_ptr模板类没有考虑不是new出来的对象,实际上shared_ptr针对这种情况设计了仿函数删除器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 仿函数的删除器
template<class T>
struct FreeFunc {
void operator()(T* ptr) {
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr) {
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main() {
FreeFunc<int> freeFunc;
std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
return 0;
}

std::shared_ptr的线程安全问题

在哪些方面存在安全隐患

  1. 引用计数
  2. 修改指向
  3. shared_ptr中的T的线程安全问题

std::shared_ptr中有两个指针,一个指向所管理的数据,一个指向控制块,这里面包括引用计数,weak_ptr的数量,删除器和分配器之类的,rc是存放在堆上的。

根据cppreference的说法,rc的加减是内存安全的

To satisfy thread safety requirements, the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed (decrementing requires stronger ordering to safely destroy the control block)

关于修改指向时的线程安全问题,依多线程访问的是不是同一个shared_ptr模板类的对象有所不同

1
2
3
4
5
std::thread td([&sp1] () {....});
``

```cpp
std::thread td([sp1] () {....});

如果std::thread的回调函数是一个lambda表达式,那么如果这里是引用捕获就有问题,拷贝就没事

或者下面这种

1
2
3
4
5
6
7
8
9
10
11
void fn(shared_ptr<A>* sp) {
...
}
...
std::thread td(fn, &sp1);

void fn(shared_ptr<A>& sp) {
...
}
...
std::thread td(fn, std::ref(sp1));

当我们在多线程回调中修改指向时,就不是线程安全的了

1
2
3
4
5
6
7
8
void fn(shared_ptr<A>& sp) {
...
if (..) {
sp = other_sp;
} else if (...) {
sp = other_sp2;
}
}

这时other_sp的rc要加1,sp的要减1,但这整个过程并不是一个原子过程

最后,如果shared_str管理的数据是STL容器这类,那么任何多线程间修改容器结构的操作都很容易导致core dump.

unique_ptr的简易实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
template<typename T>
class MyUniquePtr
{
public:
explicit MyUniquePtr(T* ptr = nullptr)
:mPtr(ptr)
{}

~MyUniquePtr()
{
if(mPtr)
delete mPtr;
}

MyUniquePtr(MyUniquePtr &&p) noexcept;
MyUniquePtr& operator=(MyUniquePtr &&p) noexcept;

MyUniquePtr(const MyUniquePtr &p) = delete;
MyUniquePtr& operator=(const MyUniquePtr &p) = delete;

T& operator*() const noexcept {return *mPtr;}
T* operator->()const noexcept {return mPtr;}
explicit operator bool() const noexcept{return mPtr;}

void reset(T* q = nullptr) noexcept
{
if(q != mPtr){
if(mPtr)
delete mPtr;
mPtr = q;
}
}

T* release() noexcept
{
T* res = mPtr;
mPtr = nullptr;
return res;
}
T* get() const noexcept {return mPtr;}
void swap(MyUniquePtr &p) noexcept
{
using std::swap;
swap(mPtr, p.mPtr);
}
private:
T* mPtr;
};

template<typename T>
MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
{
swap(*this, p);
return *this;
}

template<typename T>
MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) noexcept : mPtr(p.mPtr)
{
p.mPtr == NULL;
}

要注意bool运算符要是explicit的,不然判断ptr1==ptr2的时候,就会把两边都转化为bool值true,另外就是赋值运算符,用swap,因为自赋值情况是很少见的,用swap的话,不是自赋值的时候,另一个指针超出作用域会被自动析构,这样效率更高


shared_ptr的简易实现
http://example.com/2023/08/11/shared-ptr的简易实现/
作者
Jinming Zhang
发布于
2023年8月11日
许可协议