【C++多线程编程】 线程安全与对象生命周期管理
目录
类的线程安全
实现线程安全
构造函数在多线程中的安全性
析构函数多线程环境的安全
智能指针实现多线程安全
析构所在线程问题分析
RAII的使用
问题分析
弱回调机制的实现
类的线程安全
什么是class线程安全
- 多线程可以同时访问类的实例:线程安全的类允许多个线程同时读取或者修改类的状态,而不会引发数据不一致的问题
- 线程之间交互会不会影响类的正确行为:并发环境下线程的执行顺序不可控,操作系统可能会在任意时刻调用线程,在该环境下,很容易导致其他异常行为。如果一个类的线程是安全的,那么即使多线程交互,也不会出现数据竞争和资源争夺问题
- 无需额外的同步操作:如果一个类是线程的安全的,那么当调用这个类的时候就不需要使用锁、互斥量或者其他同步机制来确保其安全
- 例如std::string std::vector std::map等类都是线程不安全的,因为调用它们的时候需要使用锁机制
线程不安全会导致的问题
- 数据竞争:多个线程同时修改同一块内存区域,这样会导致数据不一致的情况
- 崩溃或者未定义的行为:在修改数据的时候会导致该种情况的发生
实现线程安全
构造函数在多线程中的安全性
构造函数在多线程环境下安全性
- 不要在构造函数中注册任何回调函数
- 构造函数执行的时候,对象还没有初始化完成,如果在构造函数中注册了回调函数,此时可能会被其他线程调用,这样就会导致该线程访问到了一个没有初始化完成的对象,从而会导致未定义行为或数据不一致的问题
- 不可以在构造函数中传递this指针给其他线程
- 同上,对象还没有初始化完成,就将自己的this指针给其他线程使用,是一种很不安全的行为
- 一定要确保构造函数完成后,对象已经创建,再将对象交给其他线程操作
解决方案
- 延迟泄漏this:将this指针的传递放在对象构建之后。在对象创建之后可以通过初始化函数或者工厂模式将this指针传递给其他对象或者线程,从而确保构造函数执行完毕后对象才会被外界访问
- 使用工厂函数:通过工厂函数来创建对象,在对象初始化完成后,在返回给调用者,这样就不用直接在构造函数中暴露了
错误事例参考
#include <iostream>class Observable;class Observer {
public:virtual void update() = 0; // 纯虚函数,用于接收更新通知
};class Foo : public Observer {
public:// 错误:在构造函数中直接注册 `this`,会造成线程不安全Foo(Observable* s) {s->registerObserver(this); // 将 `this` 立即注册到 `Observable`std::cout << "Foo 对象已构造并注册\n";}virtual void update() override {std::cout << "Foo 收到了更新通知\n";}private:int someData = 0; // 假设这是一个重要的初始化数据
};class Observable {
public:void registerObserver(Observer* o) {observer_ = o;std::cout << "观察者已注册\n";}void notifyObservers() {if (observer_) {observer_->update();}}private:Observer* observer_ = nullptr;
};int main() {Observable observable;Foo* foo = new Foo(&observable); // 在构造函数中注册 `this`observable.notifyObservers(); // 通知观察者,可能在对象未完全初始化时访问delete foo;return 0;
}
正确事例实现
- 延迟注册:事例中通过独立的observer()方法,将this注册到observable,确保注册发色会给你在对象构造完成后,避免未完全初始化的对象被访问
#include <iostream>class Observable;class Observer {
public:virtual void update() = 0; // 纯虚函数,用于接收更新通知
};class Foo : public Observer {
public:Foo() {// 在构造函数中仅初始化对象,不进行任何注册操作std::cout << "Foo 对象已构造\n";}// 观察者更新接口实现virtual void update() override {std::cout << "Foo 收到了更新通知\n";}// 提供一个专门的函数用于在构造后进行注册void observe(Observable* s);private:// 内部成员变量int someData = 0;
};class Observable {
public:void registerObserver(Observer* o) {observer_ = o;std::cout << "观察者已注册\n";}void notifyObservers() {if (observer_) {observer_->update();}}private:Observer* observer_ = nullptr;
};// Foo类的observe方法实现,确保注册操作在构造完成后进行
void Foo::observe(Observable* s) {s->registerObserver(this); // 仅在对象构造后再注册
}int main() {Observable observable;Foo* foo = new Foo(); // 构造对象foo->observe(&observable); // 构造完成后进行注册observable.notifyObservers(); // 通知观察者进行更新delete foo;return 0;
}
析构函数多线程环境的安全
多线程环境下的析构比单线程析构是更为复杂的,主要原因是因为其涉及到资源的竞争条件。如果在多线程环境下确保线程安全,析构函数必须小心处理对象的状态,同时与其他线程保持同步。
多线程环境下如何实现线程安全
- 避免多个线程同时读取或者写入共享状态:每个线程对共享资源的访问顺序应该是按照某种顺序执行,不可以让他们同时操作
- 成员函数的操作相互独立:函数的边界不应该发生重叠,也就是多个线程不应该同时访问同一块资源,每个成员函数的执行应该与其他函数隔离开,避免相互干扰
借助mutex无法解决问题事例
- 首先两个线程存在线程竞争:线程A在delete x之后,便将x设置成为的NULL,但是线程B在if(x)中检查了x,但是由于两个线程之间都没有同步,线程A销毁后,线程B仍然可能在销毁后访问的x,这样访问已经销毁对象,肯定会导致对应的错误
- 使用mutex无法解决问题原因分析:因为Mutex只可以保证函数内的互斥访问,但是不能控制线程之间的执行顺序,也就是说,销毁与执行的顺序是无法通过mutex来控制的
#include <iostream>
#include <thread>
#include <mutex>class Foo {
public:~Foo() {std::lock_guard<std::mutex> lock(mutex_); // 互斥锁保护析构函数std::cout << "Foo 被销毁\n";}void update() {std::lock_guard<std::mutex> lock(mutex_); // 互斥锁保护更新函数std::cout << "Foo 更新\n";}private:std::mutex mutex_;
};Foo* x = nullptr; // 全局对象指针,多个线程共享void threadA() {delete x; // 线程A销毁对象x = nullptr; // 设置 x 为 NULL
}void threadB() {if (x) { // 线程B检查 x 是否为NULLx->update(); // 如果 x 不为NULL,则调用 update()} else {std::cout << "x 是空指针\n";}
}int main() {x = new Foo(); // 初始化全局对象指针std::thread t1(threadA); // 线程A销毁对象std::thread t2(threadB); // 线程B调用 update()t1.join();t2.join();return 0;
}
mutex不仅不可以保证对象析构的线程安全,还有可能导致死锁问题
- mutex作为类成员具有局限性
- mutex可以保护对象中其他数据成员的读写,但是不能保护对象析构过程
- 由于mutex的生命周期与对象是关联的,也就是说对象如果销毁了,那么mutex也会被销毁,所以无法去为析构的时候提供保护
- 死锁
- 多个线程对多个对象进行操作的时候,如果锁的顺序不一致,这样就会导致死锁,例如线程A锁定对象a,线程B锁定对象b,双方互相等待对象释放锁的情况下,就会导致死锁
智能指针实现多线程安全
shared_ptr / weak_ptr联合使用保证其线程安全
- 引用计数的线程安全:因为两个指针都是采用原子操作的方式来维护引用计数的,因此可以保证多个线程同时增加或者的减少引用计数时的线程安全
- 访问控制:利用weak_ptr检查对象是否仍然存在,如果存在则将其提升为shared_ptr进行访问,从而确保对象在销毁前的安全访问
事例代码
- 通过shared_ptr 实现自动管理对象的生命周期,确保多线程对同一个对象的安全访问
- 使用weak_ptr避免延长对象的生命周期,与此同时使用lock()来提供安全的对象访问机制,防止访问到已经销毁的对象
#include <iostream>
#include <memory>
#include <thread>std::shared_ptr<int> globalPtr; // 全局 shared_ptrvoid threadFunc() {std::shared_ptr<int> localPtr = globalPtr; // 安全地共享对象if (localPtr) {std::cout << "Thread accessing: " << *localPtr << std::endl;}
}int main() {globalPtr = std::make_shared<int>(42); // 初始化 shared_ptrstd::thread t1(threadFunc);std::thread t2(threadFunc);t1.join();t2.join();globalPtr.reset(); // 主线程释放对象,其他线程仍然安全使用return 0;
}
shared_ptr 非完全线程安全
不能完全保证线程安全原因分析
- 引用计数线程安全:因为其引用计数是通过原子操作来实现的,也就是说多个线程可以安全的增减计数,不会竞争;同时只要还存在一个shared_ptr指向某个对象,该对象的内存就不会被释放
- 无法保证管理对象的线程安全:多线程可以安全的访问同一个shared_ptr,但是这些线程访问和修改对象的时候可能会导致数据不一致问题,例如一个线程正在写入对象,另一个线程也正在读取或者写入的时候,此时就会产生数据竞争的情况
- 读写冲突问题
- 同时读取,此时通过计数是线程安全的
- 同时写入,除非对象在写的时候进行了加锁,否则是线程不安全的
- 读写冲突:一个读一个写,可能会导致最后数据不一致的问题
事例,通过互斥锁来保证多线程访问对象的安全性
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>std::shared_ptr<int> globalPtr;
std::mutex mtx;void readSharedPtr() {std::lock_guard<std::mutex> lock(mtx);if (globalPtr) {std::cout << "Reading: " << *globalPtr << std::endl;}
}void writeSharedPtr(int value) {std::lock_guard<std::mutex> lock(mtx);if (globalPtr) {*globalPtr = value;std::cout << "Writing: " << value << std::endl;}
}int main() {globalPtr = std::make_shared<int>(42);std::thread t1(readSharedPtr);std::thread t2(writeSharedPtr, 100);t1.join();t2.join();return 0;
}
shared_ptr可能导致对象生命周期延长
shared_ptr是引用计数的智能指针,引用计数决定的对象的生命周期,但是使用过程中如果有一个指向该对象的指针没有析构,这样就会导致该对象永远不会被释放。
shared_ptr与回调函数
- 观察者设计模式中,如果通过shared_ptr来管理管理者对象,同时将这些对象放入vector容器中,此时如果不能够正确管理这些观察者对象,就可能会导致对象无法被释放
- 也就是说,如果没有手动的调用unregistyer()来将其从容器中移除shared_ptr,这些对象就会始终存放在容器中,引用计数是始终不会归零的,这样一来,对象的析构函数也就不会被调用,从而引发内存泄漏
boost::bind与shared_ptr导致问题
- 利用bind进行回调的时候,bind此时会持有一个shared_ptr的副本,所以会使得对象的引用计数增加,这也就意味着,只要回调函数仍然存在,该对象的引用计数也就永远不会归零,对象的生命周期也就会被无限延长
- 例如,如果将一个shared_ptr绑定的一个函数中,该指针的副本会一直存在,直到该函数销毁为止
事例理解
- 将oberver1绑定到function中,这样就导致了observer1的生命周期延长
- 即使移除了它,但还是不会立即销毁,只有等到function销毁的时候,observer1才会被销毁
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include <iostream>
#include <vector>
#include <memory>
#include <boost/bind.hpp>
#include <boost/function.hpp>class Observer {
public:Observer(int id) : id_(id) {std::cout << "Observer " << id_ << " created." << std::endl;}~Observer() {std::cout << "Observer " << id_ << " destroyed." << std::endl;}void onNotify() {std::cout << "Observer " << id_ << " notified." << std::endl;}private:int id_;
};class Subject {
public:void registerObserver(std::shared_ptr<Observer> obs) {observers_.push_back(obs); // 使用 shared_ptr 存储观察者}void unregisterObserver(std::shared_ptr<Observer> obs) {observers_.erase(std::remove(observers_.begin(), observers_.end(), obs), observers_.end());}void notifyAll() {for (auto& obs : observers_) {if (obs) {obs->onNotify();}}}private:std::vector<std::shared_ptr<Observer>> observers_;
};void delayedCallback(boost::function<void()> func) {std::cout << "Callback will be called after delay..." << std::endl;func(); // 延迟回调调用
}int main() {Subject subject;// 创建两个观察者并注册std::shared_ptr<Observer> observer1(new Observer(1));std::shared_ptr<Observer> observer2(new Observer(2));subject.registerObserver(observer1);subject.registerObserver(observer2);// 这里的 boost::bind 使 shared_ptr 的引用计数增加boost::function<void()> func = boost::bind(&Observer::onNotify, observer1);// 执行延迟回调delayedCallback(func);// 移除观察者subject.unregisterObserver(observer1);subject.unregisterObserver(observer2);// 通知所有观察者subject.notifyAll();// 注意:observer1 的引用计数不会为 0,因为 boost::function 持有了它的 shared_ptr 副本return 0;
}
const引用可以减少传递shared_ptr开销
const引用减少开销的原因
- 如果直接拷贝会增加引用计数,也就是说shared_ptr的引用计数会增加
- 使用const 引用的方式则不会增加计数,这样就减少了锁的开销
void onMessage(const std::string& msg) {std::shared_ptr<Foo> pFoo(new Foo(msg)); // 创建 shared_ptr,持有 Foo 实例if (validate(pFoo)) { // 通过 const 引用传递 shared_ptrsave(pFoo); // 通过 const 引用传递 shared_ptr}
}
shared_ptr 智能指针块模块的优点
优点
- 减少析构函数调用次数:因为使用shared_ptr可以自动管理对象的生命周期,不需要用户调用析构函数,所以就减少的开销
- shared_ptr可以指向任意类型的对象
- 能够管理复杂对象的生命周期
- 二进制兼容性:即使对象大小改变,旧的客户端代码依然可以兼容新版本的动态库
析构所在线程问题分析
在多线程环境下,最后拥有对象的线程不一定是初始线程,所以需要该处对最后析构线程进行分析。
- 析构线程不一定是初始化线程:资源释放的时机是在shared_ptr离开作用域的时候发生,也就是说在shared_ptr的析构函数会自动调用该对象的析构函数,但是这个析构动作发生的线程不一定是创建该对象的线程,因为对象的生命周期跨越了多个线程
- 析构发生在最后持有shared_ptr的线程:加入最后执行析构的是主线程,那么后续行为的执行,需要等待主线程执行完析构,此时就会影响性能,因为对一个对象的析构是一种十分耗时的行为
- 主线程析构问题解决思路:设计一些线程专门负责去析构对象,实现的话可以通过一个队列,将需要析构的对象都放入队列中,这样就不会影响主线程执行
RAII的使用
涉及到资源管理必然不可以绕开RAII,RAII的核心思想就是在对象构建的时候同时获取资源,并在对象销毁的时候同时释放资源。
- 避免手动释放资源:程序中如何频繁的使用new,那么就要频繁的使用delete去手动释放资源,仅仅依靠自己手动释放资源,是很容易出错的。所以要采用RAII通过自动化管理资源来减少这种错误,例如在程序中多使用shared_ptr,在其超出作用域时候自动释放其管理资源
- RAII管理共享资源:shared_ptr通过应用计数来确保对象的生命周期,确保没有任何shared_ptr持有对象的时候才释放资源。该种方式在多线程下十分适用,多线程中使用shared_ptr来共享对象,不需要手动管理对象的释放。
- 避免循环引用:在使用RAII管理资源的时候,最重要的是需要避免循环引用的情况,循环引用也就是会让引用计数永远不为零,最终引发内存泄漏。在该情况下,可以通过weak_ptr来打破循环引用,因为weak_ptr是不会影响计数的,其类似于一个不拥有对象的观察者,通过weak_ptr.lock()提升为weak_ptr,从而安全的访问对象
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <condition_variable>// 资源类,构造时分配资源,析构时释放资源
class Resource {
public:Resource() {std::cout << "资源已获取\n";}~Resource() {std::cout << "资源已释放\n";}
};// 模拟处理资源的函数
void processResource(std::shared_ptr<Resource> res) {std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟处理延迟std::cout << "正在处理资源\n";
}// 阻塞队列类,用于在线程间传递资源
template <typename T>
class BlockingQueue {
public:// 向队列中添加资源void push(T item) {std::lock_guard<std::mutex> lock(mtx);queue.push(item);cond.notify_one(); // 唤醒等待的线程}// 从队列中取出资源T pop() {std::unique_lock<std::mutex> lock(mtx);cond.wait(lock, [this]() { return !queue.empty(); }); // 等待直到队列非空T item = queue.front();queue.pop();return item;}private:std::queue<T> queue;std::mutex mtx;std::condition_variable cond;
};// 专用线程,处理资源的析构
void workerThread(BlockingQueue<std::shared_ptr<Resource>>& queue) {while (true) {auto res = queue.pop(); // 从队列中获取资源processResource(res); // 处理资源// 资源会在这里自动析构}
}int main() {// 创建阻塞队列和专用线程BlockingQueue<std::shared_ptr<Resource>> queue;std::thread worker(workerThread, std::ref(queue));// 主线程分配资源并将其传递给专用线程{auto res = std::make_shared<Resource>(); // 使用shared_ptr分配资源queue.push(res); // 将资源传递给队列,worker线程将接管资源}worker.join(); // 等待专用线程结束return 0;
}
enable_shared_from_this 使用场景分析
问题分析
当使用智能指针来管理对象的生命周期,智能指针会自动管理对象资源的分配和释放,从而避免内存泄漏。但是在某些情况下,智能指针所管理的对象需要在其成员函数内存获取一个指向自身的shared_ptr。在这种情况下如果直接使用this指针操作,可能会导致严重的生命周期管理的问题,尤其在回调函数以及多线程环境下,该问题会更明显,结合下面的事例详细理解。
要点解释说明
- Manner类:通过继承enable_shared_this,允许在Message类中获取自身shared_ptr指针,同时通过deleteResource()方法,模拟删除资源的操作
- Resource类:该类持有一个指向Manner的指针,在资源创建的时候,就将销毁资源的函数绑定为析构时的回到函数。资源被销毁的时候,这个回调函数会自动被调用
- 生命周期管理:通过shared_ptr 确保Manner的生命周期延长(重点理解)
- 也就是在Resource的析构函数中,通过使用manager的shared_ptr调用Manner的deleteResource()方法,从而保证在回调执行的时候,Manner对象依然有效
调试内容+源码
#include <iostream>
#include <memory>
#include <functional>class Manager : public std::enable_shared_from_this<Manager> {
public:// 模拟删除资源的函数void deleteResource() {std::cout << "Manager: Deleting resource\n";}// 返回当前对象的shared_ptrstd::shared_ptr<Manager> getSharedManager() {return shared_from_this();}
};class Resource {
public:Resource(std::shared_ptr<Manager> mgr) : manager(mgr) {std::cout << "Resource Created\n";// 绑定析构时的回调函数,调用Manager的deleteResource函数destructorCallback = std::bind(&Manager::deleteResource, manager);}~Resource() {std::cout << "Resource Destroyed\n";// 调用析构回调,通知ManagerdestructorCallback();}private:std::shared_ptr<Manager> manager;std::function<void()> destructorCallback;
};int main() {// 创建一个Manager对象并使用shared_ptr管理其生命周期std::shared_ptr<Manager> manager = std::make_shared<Manager>();{// 创建Resource对象,并将Manager传递给它std::shared_ptr<Resource> resource = std::make_shared<Resource>(manager);} // Resource在此作用域结束时被销毁,其析构函数被调用std::cout << "End of main\n";return 0;
}
enable_shared_from_this确保对象安全
解决上述问题,则是通过C++中提供的一个enable_shared_from_this机制来实现,其是一个模版基类,允许类的实例从其成员函数中安全的获取shared_ptr执行自身,最终实现该对象的生命周期始终是由shared_ptr来控制的。
工作原理分析
- 继承enable_shared_from_this:其是一个模版类,提供了一个shared_from_this()方法,允许类的成员函数获取一个指向该类对象的shared_ptr,只要当该对象被shared_ptr管理的时候,shared_from_this()才是有效的
- 确保生命周期延长(重点):当对象通过shared_ptr进行管理的时候,调用shared_from_this()可以确保即使所有其他地方持有shared_Ptr对象都被销毁,当前对象也仍然有效。这也就是说,成员函数或者回调函数使用该对象的时候,程序可以确保该对象不会在使用的时候销毁.
- 注意:其他地方的shared_ptr对象销毁的时候,指针引用计数不会变成0的,因为本对象中通过shared_from_this创建了一个新的shared_ptr,所以该对象不会被销毁。
代码事例理解
#include <iostream>
#include <memory>class Manager : public std::enable_shared_from_this<Manager> {
public:Manager() {std::cout << "Manager Created\n";}~Manager() {std::cout << "Manager Destroyed\n";}// 返回自身的 shared_ptrstd::shared_ptr<Manager> getSharedPtr() {return shared_from_this();}void performTask() {// 在成员函数内部获取 shared_ptrauto sharedManager = shared_from_this();std::cout << "Performing task with shared_ptr\n";// 在这里 shared_ptr 的引用计数不会变成 0,因为还有其他地方持有它}
};int main() {// Step 1: 创建一个 shared_ptr 管理 Manager 对象std::shared_ptr<Manager> manager1 = std::make_shared<Manager>();std::cout << "Reference Count after creation: " << manager1.use_count() << "\n"; // 引用计数为 1// Step 2: 调用成员函数,获取 shared_ptrmanager1->performTask();std::cout << "Reference Count after performTask: " << manager1.use_count() << "\n"; // 引用计数为 1,因为没有新增外部持有者// Step 3: 在外部再获取一个 shared_ptrstd::shared_ptr<Manager> manager2 = manager1->getSharedPtr();std::cout << "Reference Count after getSharedPtr: " << manager1.use_count() << "\n"; // 引用计数为 2// Step 4: 销毁 manager1manager1.reset(); std::cout << "Reference Count after resetting manager1: " << manager2.use_count() << "\n"; // 引用计数为 1// Step 5: 销毁 manager2manager2.reset();std::cout << "All shared_ptrs destroyed\n";return 0;
}
weak_ptr 和 shared_ptr 结合 Bind的弱回调机制(重点解决方案)
弱回调机制的实现
实现机制分析
- 将shared_ptr绑定到bind和function中的时候,其会延长绑定对象的生命周期,也就是说只有绑定的function对象销毁的时候,该指针才会销毁。通过该种方式保证了对象在回调的时候是安全,但是相应的会导致对象生命周期会比预期的更长
- 弱回调的核心思想:避免对象的生命周期过度延长,希望当对象存在的时候,可以正常的执行回调函数;如果对象销毁了,则不再执行回调
- 避免强引用带来的生命周期延长问题,则是通过weak_ptr
具体实现
- weak_ptr
- 不参与引用计数,主要就是用来打破shared_ptr的循环引用问题
- 当worker完成任务后,通过weak_ptr::lock()检查Manner是否还存在,同时根据结果来决定是否通知Manner
- 生命周期管理:利用weak_ptr::lock()检查Manner是否还存在,如果对象被销毁,则lock()则返回空指针,从而避免对已经销毁对象的访问
事例代码执行流程分析
- Manner类
- 通过继承enable_shared_from_this,让worker对象可以获取shared_ptr<Manner>,并在执行任务完成后通知后通知Manner
#include <iostream>
#include <memory>
#include <vector>// 前置声明 Manager 类
class Manager;// Worker 类负责执行任务,并在任务完成时通知 Manager
class Worker {
public:// 构造函数中传入 Manager 的 weak_ptrWorker(std::weak_ptr<Manager> manager);~Worker();// 执行任务void doWork();private:// 使用 weak_ptr 避免循环引用std::weak_ptr<Manager> manager_;// 通知 Manager 任务已完成void notifyManager();
};// Manager 类负责管理 Worker
class Manager : public std::enable_shared_from_this<Manager> {
public:Manager() {std::cout << "Manager Created\n";}~Manager() {std::cout << "Manager Destroyed\n";}// 注册 worker 完成任务的回调函数void registerWorker() {auto worker = std::make_shared<Worker>(shared_from_this());workers_.push_back(worker);worker->doWork();}void onTaskComplete() {std::cout << "Manager received task completion notification\n";}private:std::vector<std::shared_ptr<Worker>> workers_;
};// 完整定义 Worker 类的方法
Worker::Worker(std::weak_ptr<Manager> manager): manager_(manager) {std::cout << "Worker Created\n";
}Worker::~Worker() {std::cout << "Worker Destroyed\n";
}void Worker::doWork() {std::cout << "Worker is working...\n";// 模拟任务完成后通知 ManagernotifyManager();
}void Worker::notifyManager() {if (auto manager = manager_.lock()) {// manager 有效时通知manager->onTaskComplete();} else {// 如果 Manager 已销毁,通知失败std::cout << "Manager is no longer available\n";}
}int main() {{// 创建一个 Manager 对象并用 shared_ptr 管理auto manager = std::make_shared<Manager>();// 注册 Worker 并执行任务manager->registerWorker();std::cout << "Manager still exists\n";}// 当 Manager 销毁后,Worker 再次尝试访问 Manager 将失败std::cout << "End of main\n";return 0;
}
总结
- weak_ptr 和 shared_ptr 与 bind相结合实现弱回调机制,解决对象相互引用导致的生命周期问题
- weak_ptr弱化对象的引用,如果对象还存在则执行回调,如果对象不存在在不执行回调,从而保证系统的稳定性和安全性
相关文章:

【C++多线程编程】 线程安全与对象生命周期管理
目录 类的线程安全 实现线程安全 构造函数在多线程中的安全性 析构函数多线程环境的安全 智能指针实现多线程安全 shared_ptr 非完全线程安全 shared_ptr可能导致对象生命周期延长 const引用可以减少传递shared_ptr开销 shared_ptr 智能指针块模块的优点 析构所在线程…...

【系统架构设计师-2024年-上半年】综合知识-答案及详解
更多内容请见: 备考系统架构设计师-核心总结索引 文章目录 【第1题】【第2题】【第3题】【第4题】【第5题】【第6题】【第7题】【第8题】【第9题】【第10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16~17题】【第18~19题】【第20~21题】【第22题】【第23题】…...

MATLAB 中的对数计算
在 MATLAB 中,计算对数是进行数学分析和科学计算的常见需求。对数运算在数据分析、信号处理和控制系统中都有广泛应用。本篇博客将详细介绍如何在 MATLAB 中进行对数计算,包括自然对数、常用对数以及任意底数的对数。 1. 自然对数(以 e 为底…...

详解 HTTPS 与 TLS证书链校验
一文详解 HTTPS 与 TLS证书链校验_证书链怎么验证-CSDN博客 深入浅出 SSL/CA 证书及其相关证书文件(pem、crt、cer、key、csr) https://zhuanlan.zhihu.com/p/702745054...

新手做短视频素材在哪里找?做短视频素材工具教程网站有哪些?
本文将为你提供一系列新手友好的视频制作资源,包括素材网站和编辑工具,帮助你快速成为短视频领域的新星。让我们从国内知名的蛙学网开始介绍。 蛙学网:新手的视频素材天堂 对于短视频新手而言,蛙学网绝对是一个宝库。该网站提供了…...

【html】编辑器、基础、属性、标题、段落、格式化、 连接、头部、CSS、图像
目录 2.HTML编辑器 3.HTML基础 3.1 HTML标题 3.2 段落 4.HTML元素 4.1 元素语法 4.2 嵌套元素 4.3 HTML空元素 4.4 HTML提示,使用小写标签 5.HTML属性 5.1 属性实例 5.2 HTML 属性常用引用属性值 5.3 使用小写属性 5.4 HTML属性参考手册 6.HTML标题 6.1 HTML水…...

算法【洪水填充】
洪水填充是一种很简单的技巧,设置路径信息进行剪枝和统计,类似感染的过程。路径信息不撤销,来保证每一片的感染过程可以得到区分。看似是暴力递归过程,其实时间复杂度非常好,遍历次数和样本数量的规模一致。 下面通过…...

PostgreSQL的repmgr工具介绍
PostgreSQL的repmgr工具介绍 repmgr(Replication Manager)是一个专为 PostgreSQL 设计的开源工具,用于管理和监控 PostgreSQL 的流复制及实现高可用性。它提供了一组工具和实用程序,简化了 PostgreSQL 复制集群的配置、维护和故障…...

面试官:synchronized的锁升级过程是怎样的?
大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。 回答 在 JDK 1.6之前,synchronized 是一个重量级、效率比较低下的锁,但是在JDK 1.6后,JVM 为了提高锁的获取与释放效,,对 synchronized 进…...

Linux中的时间
1、date命令 参数作用参数作用参数作用%Y年xxxx%m月xx%d日xx%H小时(00~23)%M分钟(00~59)%S秒(00~59)%I小时(00~12)%t跳格[Tab键]%j今…...

用Boot写mybatis的增删改查
一、总览 项目结构: 图一 1、JavaBean文件 2、数据库操作 3、Java测试 4、SpringBoot启动类 5、SpringBoot数据库配置 二、配置数据库 在项目资源包中新建名为application.yml的文件,如图一。 建好文件我们就要开始写…...

电脑主机内存
在计算机的组成结构当中内存是非常重要的一部分,它用来存储程序和数据。对于计算机来说有了内存才能保证计算机的正常工作。 内部存储器就是我们所说的内存条,一般是用来即时存储数据。不做数据的长期保留。 外部存储器就是我们常说的固态或者硬盘。固态…...

文件操作与隐写
一、文件类型的识别 1、文件头完好情况: (1)file命令 使用file命令识别:识别出file.doc为jpg类型 (2)winhex 通过winhex工具查看文件头类型,根据文件头部内容去判断文件的类型 eg:JPG类型 &a…...

SQLException: No Suitable Driver Found - 完美解决方法详解
🚨 SQLException: No Suitable Driver Found - 完美解决方法详解 🚨 **🚨 SQLException: No Suitable Driver Found - 完美解决方法详解 🚨****摘要 📝****引言 🎯****正文 📚****1. 问题概述 ❗…...

pycharm破解教程
下载pycharm https://www.jetbrains.com/pycharm/download/other.html 破解网站 https://hardbin.com/ipfs/bafybeih65no5dklpqfe346wyeiak6wzemv5d7z2ya7nssdgwdz4xrmdu6i/ 点击下载破解程序 安装pycharm 自己选择安装路径 安装完成后运行破解程序 等到Done图标出现 选择Ac…...

如何使用 ef core 的 code first(fluent api)模式实现自定义类型转换器?
如何使用 ef core 的 code first 模式实现自定义类型转换器 前言 1. 项目结构2. 实现步骤2.1 定义转换器2.1.1 DateTime 转换器2.1.2 JsonDocument 转换器 2.2 创建实体类并配置数据结构类型2.3 定义 Utility 工具类2.4 配置 DbContext2.4.1 使用 EF Core 配置 DbContext 的两种…...

MapSet之相关概念
系列文章: 1. 先导片--Map&Set之二叉搜索树 2. Map&Set之相关概念 目录 1.搜索 1.1 概念和场景 1.2 模型 2.Map的使用 2.1 关于Map的说明 2.2 关于Map.Entry的说明 2.3 Map的常用方法说明 3.Set的说明 3.1关于Set说明 3.2 常见方法说明 1.搜…...

【大数据】浅谈Pyecharts:数据可视化的强大工具
文章目录 一、引言二、Pyecharts是什么三、Pyecharts的发展历程四、如何使用Pyecharts1. 安装Pyecharts2. 创建图表(1)导入Pyecharts模块:(2)创建图表实例:(3)添加数据:&…...

[深度学习][LLM]:浮点数怎么表示,什么是混合精度训练?
混合精度训练 混合精度训练1. 浮点表示法:[IEEE](https://zh.wikipedia.org/wiki/电气电子工程师协会)二进制浮点数算术标准(IEEE 754)1.1 浮点数剖析1.2 举例说明例子 1:例子 2: 1.3 浮点数比较1.4 浮点数的舍入 2. 混合精度训练2.1 为什么需…...

openssl双向认证自签名证书生成
编写配置文件openssl.cnf [ req ] distinguished_name req_distinguished_name req_extensions req_ext[ req_distinguished_name ] countryName Country Name (2 letter code) countryName_default US stateOrProvinceName State or Province Name…...

如何使用 Python 读取 Excel 文件:从零开始的超详细教程
“日出东海落西山 愁也一天 喜也一天 遇事不钻牛角尖” 文章目录 前言文章有误敬请斧正 不胜感恩!||Day03为什么要用 Python 读取 Excel 文件?准备工作:安装所需工具安装 Python安装 Pandas安装 openpyxl 使用 Pandas 读取 Excel 文件什么是 …...

仕考网:公务员笔试和面试哪个难?
公务员笔试和面试哪个难?二者之间考察的方向不同,难度也是不同的。 笔试部分因其广泛的知识点和有限的考试时间显得难度更高一些,在笔试环节中,考生需在有限的时间内应对各种问题,而且同时还要面对激烈的竞争,在众多…...

C++知识点总结(55):时间优化
时间优化 一、调试方法1. 输出调试2. 构造样例 二、时间优化1. 前缀和1.1 概念1.2 例题Ⅰ 区间最多数码Ⅱ 双字母字符串Ⅲ Wandering...Ⅳ 数对数目 2. 排序例题选择排序过程 一、调试方法 1. 输出调试 cout 是一个强大的调试工具,可以帮助我们查看程序的状态和变…...

GitHub每日最火火火项目(9.7)
项目名称:polarsource / polar 项目介绍:polar 是一个开源的项目,它是 Lemon Squeezy 的替代方案,具有更优惠的价格。该项目旨在让开发者能够凭借自己的热情进行编码并获得报酬。通过使用 polar,开发者可以更轻松地实现…...

11Python的Pandas:可视化
Pandas本身并没有直接的可视化功能,但它与其他Python库(如Matplotlib和Seaborn)无缝集成,允许你快速创建各种图表和可视化。这里是一些使用Pandas数据进行可视化的常见方法: 1. 使用Matplotlib Pandas中的plot()方法…...

【周易哲学】生辰八字入门讲解(二)
😊你好,我是小航,一个正在变秃、变强的文艺倾年。 🔔本文讲解【周易哲学】生辰八字入门讲解,期待与你一同探索、学习、进步,一起卷起来叭! 目录 十神十神判断十神类象十神与五行案例 地支藏干藏…...

传统CV算法——基于Opencv的多目标追踪算法
基于 OpenCV 的跟踪算法有多种,每种算法都有其特定的应用场景和优缺点。以下是一些常见的基于 OpenCV 的目标跟踪算法: 1. BOOSTING 跟踪器 描述:基于 AdaBoost 算法的跟踪器。它是一种早期的跟踪算法,使用的是基于弱分类器的强…...

人生苦短我用Python excel转csv
人生苦短我用Python excel转csv 前言准备工作pandas库主要类和方法ExcelFile 类DataFrame 类read_excel 函数to_csv 函数 示例 前言 Excel 文件和csv文件都是常用的电子表格文件格式,其中csv格式更便于用于数据交换和处理。本文使用pandas库将Excel文件转化为csv文…...

Web2和Web3笔记
KimiAI: Web2和Web3是互联网发展的不同阶段,它们代表了不同的技术、理念和用户交互方式。 Web2: Web2通常指的是第二代互联网,它始于2000年代中期,以用户生成内容和社交网络的兴起为标志。 在Web2中,用户不仅是内容的消…...

单元测试 Mock不Mock?
文章目录 前言单元测试没必要?Mock不Mock?什么是Mock?Mock的意义何在? 如何Mock?应该Mock什么?Mock 编写示例 总结 前言 前段时间,我们团队就单元测试是否采用 Mock 进行了一番交流,各有各的说法。本文就单元测试 Mock不Mock…...