当前位置: 首页 > news >正文

【C++多线程编程】 线程安全与对象生命周期管理

目录

类的线程安全

实现线程安全 

构造函数在多线程中的安全性

析构函数多线程环境的安全

智能指针实现多线程安全

 shared_ptr 非完全线程安全

shared_ptr可能导致对象生命周期延长

const引用可以减少传递shared_ptr开销

shared_ptr 智能指针块模块的优点

 析构所在线程问题分析

 RAII的使用

enable_shared_from_this 使用场景分析

问题分析

enable_shared_from_this确保对象安全

 weak_ptr 和 shared_ptr 结合 Bind的弱回调机制(重点解决方案)

弱回调机制的实现


类的线程安全

什么是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 中&#xff0c;计算对数是进行数学分析和科学计算的常见需求。对数运算在数据分析、信号处理和控制系统中都有广泛应用。本篇博客将详细介绍如何在 MATLAB 中进行对数计算&#xff0c;包括自然对数、常用对数以及任意底数的对数。 1. 自然对数&#xff08;以 e 为底…...

详解 HTTPS 与 TLS证书链校验

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

新手做短视频素材在哪里找?做短视频素材工具教程网站有哪些?

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

【html】编辑器、基础、属性、标题、段落、格式化、 连接、头部、CSS、图像

目录 2.HTML编辑器 3.HTML基础 3.1 HTML标题 3.2 段落 4.HTML元素 4.1 元素语法 4.2 嵌套元素 4.3 HTML空元素 4.4 HTML提示&#xff0c;使用小写标签 5.HTML属性 5.1 属性实例 5.2 HTML 属性常用引用属性值 5.3 使用小写属性 5.4 HTML属性参考手册 6.HTML标题 6.1 HTML水…...

算法【洪水填充】

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

PostgreSQL的repmgr工具介绍

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

面试官:synchronized的锁升级过程是怎样的?

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

Linux中的时间

1、date命令 参数作用参数作用参数作用%Y年xxxx%m月xx%d日xx%H小时&#xff08;00&#xff5e;23&#xff09;%M分钟&#xff08;00&#xff5e;59&#xff09;%S秒&#xff08;00&#xff5e;59&#xff09;%I小时&#xff08;00&#xff5e;12&#xff09;%t跳格[Tab键]%j今…...

用Boot写mybatis的增删改查

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

电脑主机内存

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

文件操作与隐写

一、文件类型的识别 1、文件头完好情况&#xff1a; &#xff08;1&#xff09;file命令 使用file命令识别&#xff1a;识别出file.doc为jpg类型 &#xff08;2&#xff09;winhex 通过winhex工具查看文件头类型&#xff0c;根据文件头部内容去判断文件的类型 eg:JPG类型 &a…...

SQLException: No Suitable Driver Found - 完美解决方法详解

&#x1f6a8; SQLException: No Suitable Driver Found - 完美解决方法详解 &#x1f6a8; **&#x1f6a8; SQLException: No Suitable Driver Found - 完美解决方法详解 &#x1f6a8;****摘要 &#x1f4dd;****引言 &#x1f3af;****正文 &#x1f4da;****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之相关概念

系列文章&#xff1a; 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. 创建图表&#xff08;1&#xff09;导入Pyecharts模块&#xff1a;&#xff08;2&#xff09;创建图表实例&#xff1a;&#xff08;3&#xff09;添加数据&#xff1a;&…...

[深度学习][LLM]:浮点数怎么表示,什么是混合精度训练?

混合精度训练 混合精度训练1. 浮点表示法&#xff1a;[IEEE](https://zh.wikipedia.org/wiki/电气电子工程师协会)二进制浮点数算术标准&#xff08;IEEE 754&#xff09;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 文件:从零开始的超详细教程

“日出东海落西山 愁也一天 喜也一天 遇事不钻牛角尖” 文章目录 前言文章有误敬请斧正 不胜感恩&#xff01;||Day03为什么要用 Python 读取 Excel 文件&#xff1f;准备工作&#xff1a;安装所需工具安装 Python安装 Pandas安装 openpyxl 使用 Pandas 读取 Excel 文件什么是 …...

仕考网:公务员笔试和面试哪个难?

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

C++知识点总结(55):时间优化

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

GitHub每日最火火火项目(9.7)

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

11Python的Pandas:可视化

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

【周易哲学】生辰八字入门讲解(二)

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

传统CV算法——基于Opencv的多目标追踪算法

基于 OpenCV 的跟踪算法有多种&#xff0c;每种算法都有其特定的应用场景和优缺点。以下是一些常见的基于 OpenCV 的目标跟踪算法&#xff1a; 1. BOOSTING 跟踪器 描述&#xff1a;基于 AdaBoost 算法的跟踪器。它是一种早期的跟踪算法&#xff0c;使用的是基于弱分类器的强…...

人生苦短我用Python excel转csv

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

Web2和Web3笔记

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

单元测试 Mock不Mock?

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