线程安全的对象生命周期管理
禁止拷贝
class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
// 禁止拷贝构造
NonCopyable(const NonCopyable&) = delete;
// 禁止拷贝赋值运算符
NonCopyable& operator=(const NonCopyable&) = delete;
};
NonCopyable a;
NonCopyable b = a; // 错误,拷贝构造被禁用
NonCopyable c;
c = a; // 错误,拷贝赋值运算符被禁用
mutable 关键字
在 C++ 中,mutable 关键字允许你在 const 成员函数 或 const 对象 中修改被标记为 mutable 的成员变量。通常,在 const 对象或 const 成员函数中,类的成员变量是不可修改的,但通过使用 mutable,可以允许对某些特 定成员变量进行修改,即使它们属于 const 对象。
mutable 关键字主要用于以下场景:
- 缓存计算结果:在
const成员函数中可以计算结果并存储在mutable成员变量中。 - 实现线程同步:当一个对象是
const的,但是你仍然希望使用mutex来实现线程同步,你可以将mutex声明为mutable。
成员 mutex 无法保护析构
例如:
Foo::~Foo() {
std::lock_guard<std::mutex> guard(this->mutex);
// free internal state (1)
}
void Foo::update() {
std::lock_guard<std::mutex> guard(this->mutex); // (2)
// make use of internal state
}
// thread A
delete x;
x = nullptr;
// thread B
if (x) {
x->update();
}
当线程 A 执行到 (1) 处,线程 B 执行阻塞在 (2) 处,就可能发生崩溃。
成员 mutex 无法保证析构安全,析构是发生在对象身故之时,而成员 mutex 的生命周期是和对象一样长的。此外对于基类对象,当调用到基类析构时,派生类部分已经析构了,基类对象的 mutex 不可能保护派生类的析构。
只有在别的线程都访问不到这个对象时,析构才是安全的。
成员 mutex 可能导致死锁问题
一个函数如果要锁住相同类型的多个对象,例如在实现多线程的 swap、拷贝赋值时,可能存在死锁问题。
例如:
#include <iostream>
#include <thread>
#include <mutex>
class Counter {
public:
Counter(int value = 0) : value_(value) {}
void increase() {
std::lock_guard<std::mutex> guard(this->mutex_);
this->value_ += 1;
}
Counter& operator=(const Counter& rhs) {
if (this == &rhs) {
return *this;
}
std::lock_guard<std::mutex> guardA(this->mutex_);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟延迟
std::lock_guard<std::mutex> guardB(rhs.mutex_);
this->value_ = rhs.value_;
return *this;
}
static void swap(Counter &a, Counter &b) {
// 锁定 a 的 mutex
std::lock_guard<std::mutex> guardA(a.mutex_);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟延迟
// 锁定 b 的 mutex
std::lock_guard<std::mutex> guardB(b.mutex_);
// 交换 a 和 b 的值
int value = a.value_;
a.value_ = b.value_;
b.value_ = value;
}
int getValue() const { return value_; }
private:
int value_;
mutable std::mutex mutex_;
};
void threadFunc(Counter &a, Counter &b) {
Counter::swap(a, b);
}
void threadFunc2(Counter &a, Counter &b) {
a = b;
}
int main() {
Counter counterA(1), counterB(2);
{
// 创建两个线程,分别交换 counterA 和 counterB
std::thread t1(threadFunc, std::ref(counterA), std::ref(counterB));
std::thread t2(threadFunc, std::ref(counterB), std::ref(counterA));
// 等待线程完成
t1.join();
t2.join();
}
{
// 创建两个线程,分别赋值给对方
std::thread t1(threadFunc2, std::ref(counterA), std::ref(counterB));
std::thread t2(threadFunc2, std::ref(counterB), std::ref(counterA));
// 等待线程完成
t1.join();
t2.join();
}
std::cout << "Counter A: " << counterA.getValue() << std::endl;
std::cout << "Counter B: " << counterB.getValue() << std::endl;
return 0;
}
std::lock
这时需要保证始终按照系统顺序加锁,例如比较 mutex 对象的地址,始终先加锁地址比较小的 mutex。或者使用 std::lock。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
class Counter {
public:
Counter(int value = 0) : value_(value) {}
void increase() {
std::lock_guard<std::mutex> guard(this->mutex_);
this->value_ += 1;
}
static void swap(Counter &a, Counter &b) {
// 同时尝试锁定 a 和 b 的 mutex,避免死锁
std::lock(a.mutex_, b.mutex_);
// 使用 std::lock_guard 确保锁会自动释放,adopt_lock 表示锁已经被锁定
std::lock_guard<std::mutex> guardA(a.mutex_, std::adopt_lock);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟延迟
std::lock_guard<std::mutex> guardB(b.mutex_, std::adopt_lock);
// 交换 a 和 b 的值
int temp = a.value_;
a.value_ = b.value_;
b.value_ = temp;
}
int getValue() const { return value_; }
private:
int value_;
mutable std::mutex mutex_;
};
void threadFunc(Counter &a, Counter &b) {
Counter::swap(a, b);
}
int main() {
Counter counterA(1), counterB(2);
// 创建两个线程,分别交换 counterA 和 counterB
std::thread t1(threadFunc, std::ref(counterA), std::ref(counterB));
std::thread t2(threadFunc, std::ref(counterB), std::ref(counterA));
// 等待线程完成
t1.join();
t2.join();
std::cout << "Counter A: " << counterA.getValue() << std::endl;
std::cout << "Counter B: " << counterB.getValue() << std::endl;
return 0;
}
析构专用线程
如果 shared_ptr 析构比较耗时,可能会拖慢关键线程的速度(如果 shared_ptr 引起的析构发生在关键线程中)。可以用一个专门线程做析构,通过 BlockingQueue<shared_ptr<void>> 把对象析构都移动到那个专有线程,从而释放关键线程。
如下代码中主线程将不再使用的 Resource 指针通过 destructorThread.enqueue(resource) 扔给专门的析构线程,在析构线程中循环不断 auto ptr = queue_.pop(),这样析构就会发生在该析构线程中。
#include <iostream>
#include <thread>
#include <memory>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <chrono>
class BlockingQueue {
public:
void push(std::shared_ptr<void> ptr) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(ptr);
cond_var_.notify_one();
}
std::shared_ptr<void> pop() {
std::unique_lock<std::mutex> lock(mutex_);
cond_var_.wait(lock, [this]() { return !queue_.empty(); });
auto ptr = queue_.front();
queue_.pop();
return ptr;
}
private:
std::queue<std::shared_ptr<void>> queue_;
std::mutex mutex_;
std::condition_variable cond_var_;
};
class DestructorThread {
public:
DestructorThread() : stop_(false) {
thread_ = std::thread([this]() { this->run(); });
}
~DestructorThread() {
stop_ = true;
queue_.push(nullptr); // Push a null pointer to wake up the thread
thread_.join();
}
void enqueue(std::shared_ptr<void> ptr) {
queue_.push(ptr);
}
private:
void run() {
while (!stop_) {
auto ptr = queue_.pop();
if (ptr) {
// Simulate a costly destruction process
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Destroyed resource." << std::endl;
}
}
}
std::thread thread_;
BlockingQueue queue_;
bool stop_;
};
class Resource {
public:
Resource(int id) : id_(id) {
std::cout << "Resource " << id_ << " created." << std::endl;
}
~Resource() {
std::cout << "Resource " << id_ << " destructed." << std::endl;
}
private:
int id_;
};
int main() {
DestructorThread destructorThread;
// Simulate creating shared resources
for (int i = 0; i < 5; ++i) {
auto resource = std::make_shared<Resource>(i);
destructorThread.enqueue(resource);
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Simulate work in the main thread
}
// Wait a bit for all resources to be destructed
std::this_thread::sleep_for(std::chrono::seconds(3));
return 0;
}
std::enable_shared_from_this
对于一个原生指针,只能被一个 shared_ptr 实例直接管理(初始化),如果一个原生指针被多个 shared_ptr 管理会导致内存被一个 shared_ptr 释放而另一个 shared_ptr 还在访问该地址的问题。
所以引入 std::enable_shared_from_this,通过继承 std::enable_shared_from_this,你可以在类的成员函数中调用 shared_from_this() 方法来获取一个 shared_ptr,该指针共享对象的所有权。
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> getShared() {
return shared_from_this();
}
void show() {
std::cout << "MyClass::show() called!" << std::endl;
}
};
int main() {
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> sp2 = sp1->getShared(); // 获取一个共享的 shared_ptr
sp1->show();
sp2->show();
std::cout << "sp1 use count: " << sp1.use_count() << std::endl;
std::cout << "sp2 use count: " << sp2.use_count() << std::endl;
return 0;
}
/* 输出
MyClass::show() called!
MyClass::show() called!
sp1 use count: 2
sp2 use count: 2
*/
弱回调
弱回调技术(Weak Callback)旨在避免使用回调函数时出现对象生命周期管理的问题,尤其是避免悬空指针(dangling pointer)或资源泄露的风险。弱回调技术主要用于防止回调函数中访问已经销毁的对象。通过使用 std::weak_ptr 来管理对象的生命周期。当回调函数需要使用对象时,会首先通过 std::weak_ptr 尝 试获取 std::shared_ptr,确保对象在回调时依然存在。
#include <iostream>
#include <memory>
#include <functional>
class Worker {
public:
void doWork() {
std::cout << "Working..." << std::endl;
}
};
class Manager {
public:
void setCallback(std::weak_ptr<Worker> worker) {
callback_ = [worker]() {
if (auto sp = worker.lock()) {
sp->doWork(); // 只有在对象存在时才调用
} else {
std::cout << "Worker object has been destroyed." << std::endl;
}
};
}
void executeCallback() {
if (callback_) {
callback_();
}
}
private:
std::function<void()> callback_;
};
int main() {
Manager manager;
{
auto worker = std::make_shared<Worker>();
manager.setCallback(worker); // 设置回调
manager.executeCallback(); // 调用回调,worker 存在
} // worker 被销毁
manager.executeCallback(); // 再次调用回调,worker 已被销毁
return 0;
}
/* 输出
Working...
Worker object has been destroyed.
*/