多线程下对象销毁与智能指针
参考:
- Linux多线程服务器编程 陈硕
1. 概述
在多线程环境下,对象被多个线程共享,这时对象的销毁成为一个问题。在程序遇到较大并发时,常常会因为对象已经析构了,但是另一个线程还会去调用对象的情况。
本文主要参考 <Linux多线程服务器编程 陈硕> 第一章相关内容,学习记录。
2. 析构与锁
锁无法保护对象析构:
class A {
public:
~A {
std::lock_quard<std::mutex> lq(lock);
//
// do something
//
}
void func() {
std::lock_quard<std::mutex> lq(lock);
//
// do something
//
}
private:
std::mutex lock;
};
如上 class A 的析构函数中加锁,本意可能是为了保护析构过程,但是实际上,另一个线程可能正在进入 A::func() 函数,对象析构完成后 A::lock 互斥体也释放了,这时 A::func() 中加锁的行为就是未定义的。
实际上从语义上来说,一个对象正在析构或者已经析构,不应该还有别的线程再使用此对象。所以析构函数中不应该存在加锁行为,锁只应该出现在类普通成员函数中。
3. 对象安全销毁
3.1 线程调用对象函数的 2 种情况
一个线程调用对象的成员函数分为两种典型情况:
- 线程通过一个第三方管理类,来获取目标对象,再调用成员函数
- 线程通过注册的回调来调用对象成员函数
3.1.1 第三方管理类
这种方式第三方管理类需要知道目标对象是否正在析构或者已经析构,如下:
template <class T>
class Manager {
public:
Manager(T* _obj) {
obj = _obj;
}
T* get() {
//
// if (obj 还有效)
// 返回 obj
// else
// 返回 nullptr
}
private:
T* obj;
};
3.1.2 回调
这种方式在回调发生时,对象必须还有效:
class B {
public:
void func() {
//
// do something
//
}
void wait_job(AsyncTask& async_task) {
auto callback = std::bind(&B::func, this);
async_task.do_job(callback);
}
};
3.2 获取对象是否有效
如何知道对象正在析构或者已经析构呢,很明显我们无法通过指针或者引用来获知。
实际上我们可以借助智能指针 std::shared_ptr 与 std::weak_ptr 来感知对象是否有效:
template <class T>
class Manager {
public:
Manager(std::shared_ptr<T> _obj) {
obj = _obj;
}
std::shared_ptr<T> get() {
return obj.lock();
}
private:
std::weak_ptr<T> obj;
};
std::mutex job_lock;
void do_job(Manager& manager) {
// 注意,在获取 ptr 时,需要加锁
std::shared_ptr<A> ptr;
{
std::lock_quard<std::mutex> lq(job_lock);
ptr = manager.get();
}
if (ptr) { // 运用 std::shared_ptr::operator bool()
// 如果对象还有效,调用其成员函数
ptr->func();
} else {
// do else
}
}
如上,Manager 类对象使用 std::weak_ptr 来持有对 A 对象的弱引用,在调用 Manager::get() 方法的时候,通过对 std::weak_ptr 提升,可以检查对象 A 是否正在销毁或已经销毁。
3.3 提升对象有效期
回调发生时,需要确保对象有效,可以借助 std::shared_ptr 延迟销毁对象:
class B : public std::enable_shared_from_this<B> {
public:
void func() {
//
// do something
//
}
std::shared_ptr<B> get_shared() {
return shared_from_this();
}
void wait_job(AsyncTask& async_task) {
auto callback = std::bind(&B::func, get_shared());
async_task.do_job(callback);
}
};
以上这种方式,回调对象绑定时,增加了对象 B 的引用计数,即增加了对象的生命周期,直到回调被调用后,对象才可能真正释放。
这种方法是侵入式的,即类必须继承 std::enable_shared_from_this 基类,此基类在第一次构造 std::shared_ptr 时,初始化了内部的一个 weak_ptr 对象,借助继承的 shared_from_this() 函数,提升 weak_ptr 为 shared_ptr,此特性让一个被 shared_ptr 管理的对象也能知道自己的引用计数情况。