C++17事件总线实现:类型安全、线程安全的发布订阅模式
2026/7/11 12:47:47 网站建设 项目流程

1. 项目概述:为什么我们需要一个带参数的事件总线?

在C++项目里,尤其是那些模块多、交互复杂的系统,比如游戏引擎、GUI框架或者微服务架构,组件间的通信一直是个头疼事。你肯定遇到过这种场景:用户界面上的一个按钮被点击了,需要通知后台逻辑更新数据,同时还要刷新另一个窗口的状态。如果直接用函数调用,组件之间就产生了强耦合,A模块必须知道B模块的存在和接口,改一处代码可能牵一发动全身,测试和维护都成了噩梦。

这时候,事件驱动架构(Event-Driven Architecture)就派上用场了。它的核心思想是“订阅-发布”(Pub/Sub):事件的“发布者”只管发出信号,说“某某事发生了”,它不关心谁听、有多少人听;事件的“订阅者”则声明自己对哪些事感兴趣,并提供一个回调函数来处理。两者通过一个中间人——也就是“事件总线”(Event Bus)——来连接,完全解耦。

但光有事件名还不够。一个“用户登录”事件,如果只告诉你“登录发生了”,你很可能还需要知道是哪个用户、从哪个IP登录的、登录时间是什么。这就是“带参数的事件总线”要解决的问题。它允许事件携带任意类型、任意数量的数据载荷(Payload),订阅者的回调函数能接收到这些具体的参数,从而进行有针对性的处理。

我最近在重构一个老项目的消息系统时,就决定自己动手实现一个。市面上虽然有一些库,但要么太重,引入了我不需要的依赖;要么功能不全,比如线程安全做得不好,或者参数传递限制太多。我的目标是打造一个轻量、高效、类型安全且线程安全的C++17事件总线,它得是单头文件(Header-only)的,方便集成;同时要支持运行时的参数类型匹配,让用起来更灵活。接下来,我就把这个从设计到实现的全过程,以及踩过的坑和总结的经验,详细分享给你。

2. 核心设计思路与架构选型

2.1 同步 vs. 异步:为什么我选择了同步模型?

事件总线的分发模型主要有两种:同步和异步。异步模型通常会把事件丢到一个队列里,由后台线程池消费,发布者可以立即返回,不阻塞。这听起来很美好,能提高响应性,对吧?但在实际项目中,我最终选择了同步模型,原因有这么几点:

首先,复杂度。异步引入了队列管理、线程池、任务调度、背压(Backpressure)处理等一系列问题。事件处理的顺序可能变得不确定(除非用顺序队列),调试和复现问题会困难很多。对于许多业务逻辑,特别是需要保证操作原子性或顺序性的场景,同步模型更简单、更可控。

其次,错误处理。在同步模型中,publish()调用会等待所有订阅者的回调执行完毕,并返回一个结果结构,告诉你成功调用了几个、失败(抛出异常)了几个。调用方可以立即知道事件处理的状态。而在异步模型中,错误通常需要额外的回调或Future机制来传递,链路变长,逻辑更复杂。

第三,资源与生命周期。异步调用时,如果事件参数或回调捕获的对象在事件被处理前就被销毁了,会导致悬空引用,这是很难排查的崩溃隐患。同步模型在调用栈上完成一切,生命周期清晰,资源管理更直接。

当然,同步模型的缺点是可能阻塞发布者线程,如果某个订阅者的回调执行非常耗时,会影响整体响应。我的设计哲学是:事件总线只负责可靠、正确地将事件分发给订阅者,至于耗时操作,应该由订阅者自己决定是否要转移到其他线程去执行。例如,订阅者收到事件后,可以简单地将任务派发到自己的线程池。这样,职责更清晰,事件总线本身保持轻量和可预测。

2.2 类型安全与参数传递:核心挑战与方案

实现带参数事件总线的最大挑战在于类型擦除(Type Erasure)。订阅时,我们不知道用户会传入什么类型的回调函数;发布时,我们也不知道用户会传入什么类型的参数。但最终,我们需要在运行时检查类型是否匹配,并将参数正确地传递给回调。

C++标准库中的std::function虽然能存储可调用对象,但它要求签名固定。我们无法用一个std::function<void(std::any)>来存储void(int, std::string)这样的回调,因为调用方式完全不同。

我的解决方案是借鉴了std::anystd::tuple的组合。基本思路如下:

  1. 订阅时:将用户提供的回调函数(Callable)包装成一个内部函数对象Invokable。这个Invokable的调用签名是统一的,例如void(const std::any&)。在包装过程中,我们会记录下原始回调的函数签名类型信息(利用std::functiontarget_type或自定义类型描述符)。
  2. 存储时:将Invokable和它的类型信息一起,以std::function<void(const std::any&)>的形式存储在对应事件名的订阅列表中。这样,存储容器是类型统一的。
  3. 发布时:用户提供的参数被打包成一个std::tuple<Args...>。然后,这个tuple被放入一个std::any中。对于该事件的每一个订阅项,我们从std::any中尝试提取出tuple,并利用编译期类型推导和运行时检查,判断存储的回调类型是否与当前tuple的类型匹配。
  4. 调用时:如果类型匹配,则通过一系列模板技巧(如std::apply)将tuple解包,调用原始的、类型正确的回调函数。这个过程对用户完全透明。

这个方案的关键在于运行期类型匹配。我们不能仅仅在编译期检查,因为事件名和回调的绑定是动态的。我们需要在publish的时候,判断std::any里装的tuple<Args...>是否能够转换为订阅者所期望的参数类型tuple<Params...>。这涉及到判断两个类型列表是否在某种意义上是“兼容”的,例如允许const char*std::string的转换。

2.3 线程安全设计:锁的粒度与生命周期管理

一个健壮的事件总线必须考虑多线程环境。多个线程可能同时订阅、取消订阅、发布事件。我的设计目标是保证订阅表(Subscriber Map)的线程安全,同时避免在调用用户回调时持有锁,以防止死锁和性能瓶颈。

具体实现如下:

  • 数据结构:使用std::unordered_map<std::string, SubscriberList>作为核心存储。每个事件名对应一个订阅者列表。
  • 锁的选择:使用std::shared_mutex(C++17)。这允许多个线程同时读取订阅表(例如并发发布),但写入操作(订阅、取消订阅、清空)需要独占锁。这比普通std::mutex的并发度更高。
  • 发布流程的锁策略
    1. 发布开始时,获取共享锁,对指定事件的订阅者列表做一份快照(例如拷贝一份std::vector<CallbackItem>)。这个操作很快。
    2. 立即释放共享锁。后续的操作不再持有任何与订阅表相关的锁。
    3. 遍历快照中的每一个回调项,进行类型检查并执行调用。因为快照是独立的,即使其他线程在此期间修改了原始订阅表(增删订阅者),也不会影响本次发布的遍历过程,保证了本次发布视图的一致性。
  • 取消订阅与关闭的等待机制:当调用unsubscribeclose时,需要确保目标回调函数不会在“即将被调用”或“正在执行”的状态下被暴力移除。我采用的方法是:每个回调项都有一个“活动状态”标志。取消订阅时,先将该标志置为无效,然后等待所有当前正在执行该回调的线程退出(通过一个条件变量或计数器)。这保证了回调函数执行期间,其所需的资源(比如捕获的this指针)是有效的,避免了析构后调用的致命错误。

注意:这种设计意味着,事件总线只保证了自身数据结构的线程安全。用户回调函数内部的业务逻辑如果访问共享数据,必须由用户自己加锁保护。因为同一个回调可能被多个发布线程同时执行。

3. 关键实现细节拆解

3.1 核心数据结构:如何存储异构的回调?

让我们深入到代码层面。首先,我们需要一个类型来封装用户回调及其元信息。

namespace eventbus::detail { struct CallbackWrapperBase { using Id = std::size_t; Id id {0}; std::atomic<bool> active {true}; std::function<void(const std::any&)> invoker; std::type_index signatureType; // 用于类型匹配 virtual ~CallbackWrapperBase() = default; }; template <typename Func> struct CallbackWrapper final : public CallbackWrapperBase { Func callback; // 构造时,需要创建一个 invoker,它知道如何将 any 中的 tuple 解包并调用 callback explicit CallbackWrapper(Func&& f) : callback(std::forward<Func>(f)) { // ... 初始化 invoker 和 signatureType } }; }

这里的关键是invoker,它是一个std::function<void(const std::any&)>。我们需要在构造CallbackWrapper时,根据Func的具体类型,生成一个对应的invoker。这个生成过程是编译期完成的,利用了模板元编程。

template<typename Callback, typename... Args> void make_invoker(CallbackWrapper<Callback>* wrapper) { wrapper->invoker = [callback = &(wrapper->callback)](const std::any& packedArgs) { // 1. 检查 any 中是否包含了 tuple<Args...> if constexpr (sizeof...(Args) > 0) { if (auto* tuplePtr = std::any_cast<std::tuple<std::decay_t<Args>...>>(&packedArgs)) { // 2. 使用 std::apply 解包 tuple 并调用原始回调 std::apply(*callback, *tuplePtr); } else { // 类型不匹配,可以记录日志或增加计数 throw std::bad_any_cast(); } } else { // 无参数事件,直接调用 (*callback)(); } }; // 记录类型信息,用于快速匹配筛选 wrapper->signatureType = typeid(std::tuple<std::decay_t<Args>...>); }

在实际的subscribe函数中,我们需要推导出回调Func的签名,并提取其参数类型Args...,然后调用make_invoker。这可以通过函数 traits 技巧实现。

3.2 类型匹配与参数转换:std::anystd::tuple的魔法

发布函数publish的签名是template <typename... Args> PublishResult publish(const std::string& eventName, Args&&... args)。它的任务是将args...打包,并找到匹配的订阅者。

template <typename... Args> PublishResult EventBus::publish(const std::string& eventName, Args&&... args) { // 1. 将参数打包成 tuple,并利用 decay 去除引用和cv限定符,便于存储和匹配 using ArgsTuple = std::tuple<std::decay_t<Args>...>; auto argsTuple = std::make_tuple(std::forward<Args>(args)...); std::any packedArgs(std::move(argsTuple)); // 2. 获取订阅者快照(内部会加共享锁) auto snapshot = getSnapshot(eventName); PublishResult result; result.subscribers = snapshot.size(); // 3. 遍历快照 for (auto& weakWrapper : snapshot) { // 使用弱引用,避免影响生命周期 if (auto wrapper = weakWrapper.lock()) { if (!wrapper->active.load(std::memory_order_acquire)) { ++result.skipped; continue; } // 4. 关键:类型匹配检查 // 我们期望的目标类型是 wrapper->signatureType // 当前参数类型是 typeid(ArgsTuple) if (isTypeCompatible(wrapper->signatureType, typeid(ArgsTuple))) { try { wrapper->invoker(packedArgs); // 调用统一的 invoker ++result.invoked; } catch (...) { ++result.failed; // 记录异常日志,但不要抛出,保证其他订阅者被执行 } } else { ++result.type_mismatches; } } } return result; }

isTypeCompatible是运行期类型匹配的核心。简单的实现可以直接比较typeid是否相等,但这太严格了,不支持const char*std::string这样的隐式转换。为了支持有限的、安全的类型转换,我们需要一个更复杂的匹配逻辑。一个实用的方法是维护一个类型兼容性映射表,在编译期注册哪些类型之间可以转换。例如:

bool isTypeCompatible(const std::type_index& target, const std::type_index& source) { if (target == source) return true; // 检查兼容性映射 static std::unordered_map<std::type_index, std::vector<std::type_index>> compatibilityMap = { {typeid(std::string), {typeid(const char*), typeid(char*), typeid(std::string_view)}}, {typeid(std::string_view), {typeid(const char*), typeid(char*), typeid(std::string)}}, // ... 可以扩展其他兼容类型,如数值类型之间的转换(int -> double) }; auto it = compatibilityMap.find(target); if (it != compatibilityMap.end()) { return std::find(it->second.begin(), it->second.end(), source) != it->second.end(); } return false; }

invoker内部,当检测到类型兼容但并非完全相等时,需要构造一个符合目标类型的tuple。例如,当回调期望std::string,而发布者传递const char*时,invoker需要从any中取出tuple<const char*>,然后构造一个新的tuple<std::string>,再进行调用。这增加了运行时的开销,但提供了更大的灵活性。

3.3 订阅者生命周期与资源管理:智能指针与弱引用

这是事件总线中最容易出错的地方。订阅者(通常是一个对象)将其成员函数注册为回调。如果订阅者对象先于事件总线被销毁,那么当事件触发时,回调就会操作一个无效的this指针,导致未定义行为(通常是崩溃)。

我的解决方案是强制使用智能指针来管理生命周期,并在事件总线内部使用std::weak_ptr来引用回调包装器。

对于订阅者对象

  • 建议用户使用std::shared_ptr管理订阅者对象。
  • 在订阅时,使用std::weak_ptr来捕获对象。
class MyService : public std::enable_shared_from_this<MyService> { public: void init(EventBus& bus) { auto weak_self = weak_from_this(); // 获取 weak_ptr bus.subscribe("data_ready", [weak_self](const Data& d) { if (auto self = weak_self.lock()) { // 尝试提升为 shared_ptr self->processData(d); } // 如果提升失败,说明对象已销毁,安静地忽略此次事件 }); } void processData(const Data& d) { /* ... */ } };

在事件总线内部

  • 订阅列表存储的不是CallbackWrapper的裸指针,而是std::weak_ptr<CallbackWrapperBase>
  • 获取快照时,将弱引用提升为共享指针。提升失败意味着该回调包装器已被释放(例如在clear()时),直接跳过。
  • 这种设计使得EventBus的析构或clear()操作变得简单:它只需要清空std::unordered_map,所有CallbackWrappershared_ptr引用计数归零,资源自动释放。而正在执行的回调,由于其持有shared_ptr,会保证包装器在其执行期间存活。
std::vector<std::shared_ptr<detail::CallbackWrapperBase>> EventBus::getSnapshot(const std::string& eventName) { std::shared_lock lock(mutex_); std::vector<std::shared_ptr<detail::CallbackWrapperBase>> snapshot; if (auto it = subscribers_.find(eventName); it != subscribers_.end()) { for (const auto& weakWrapper : it->second) { if (auto wrapper = weakWrapper.lock()) { snapshot.push_back(wrapper); } } } return snapshot; }

4. 完整实现与核心API解析

4.1 主要类与接口设计

基于以上设计,我们可以勾勒出EventBus类的主要接口。

namespace eventbus { class EventBus { public: using CallbackId = std::size_t; struct PublishResult { std::size_t subscribers {0}; // 快照中的订阅数 std::size_t invoked {0}; // 成功调用数 std::size_t failed {0}; // 抛出异常的回调数 std::size_t type_mismatches {0}; // 参数类型不匹配数 std::size_t skipped {0}; // 已取消激活的订阅数 }; // 构造与析构 EventBus(); ~EventBus(); EventBus(const EventBus&) = delete; EventBus& operator=(const EventBus&) = delete; // 核心功能 template <typename Callback> CallbackId subscribe(const std::string& eventName, Callback&& callback); bool unsubscribe(const std::string& eventName, CallbackId id); std::size_t unsubscribeAll(const std::string& eventName); template <typename... Args> PublishResult publish(const std::string& eventName, Args&&... args); template <typename... Args> bool publishIfMinSubscribers(const std::string& eventName, std::size_t minSubscribers, Args&&... args); // 状态查询 void close(); void clear(); bool isClosed() const; std::size_t getCallbackCount(const std::string& eventName) const; // ... 其他统计接口 private: mutable std::shared_mutex mutex_; std::unordered_map<std::string, std::vector<std::weak_ptr<detail::CallbackWrapperBase>>> subscribers_; std::atomic<std::size_t> nextCallbackId_ {1}; std::atomic<bool> closed_ {false}; // ... 其他私有成员和 helper 函数 }; }

4.2subscribe的实现:类型擦除与包装

subscribe是入口,它需要处理任意可调用对象。

template <typename Callback> EventBus::CallbackId EventBus::subscribe(const std::string& eventName, Callback&& callback) { if (closed_.load(std::memory_order_acquire)) { return 0; // 已关闭,订阅失败 } using CallbackType = std::decay_t<Callback>; using Traits = detail::CallbackTraits<CallbackType>; // 1. 创建包装器,并用 shared_ptr 管理其生命周期 auto wrapper = std::make_shared<detail::CallbackWrapper<CallbackType>>( std::forward<Callback>(callback) ); wrapper->id = nextCallbackId_.fetch_add(1, std::memory_order_relaxed); // 2. 利用 Traits 提取参数类型,初始化 invoker detail::setupInvoker<Traits>(wrapper.get()); // 3. 加独占锁,插入订阅表 { std::unique_lock lock(mutex_); subscribers_[eventName].push_back(wrapper); } return wrapper->id; }

这里的detail::CallbackTraits是一个类型萃取(Type Traits)类,用于获取可调用对象的返回类型和参数类型。对于函数指针、成员函数指针、lambda、std::function等,需要特化处理。

detail::setupInvoker是一个模板函数,它根据Traits提供的参数类型列表,实例化之前提到的make_invoker函数,完成invoker的初始化。

4.3publish的实现:快照、匹配与调用

publish的实现前文已经给出了主要框架。这里补充一下getSnapshot和遍历调用中的一些细节。

template <typename... Args> EventBus::PublishResult EventBus::publish(const std::string& eventName, Args&&... args) { if (closed_.load(std::memory_order_acquire)) { return {}; // 返回空的 PublishResult } PublishResult result; auto snapshot = getSnapshot(eventName); // 内部加共享锁并复制 result.subscribers = snapshot.size(); if (snapshot.empty()) { // 可以触发一个警告日志,如果开启了 verbose 模式 return result; } // 打包参数。注意,这里使用 decay 后的类型存储,避免引用悬空。 using ArgsTuple = std::tuple<std::decay_t<Args>...>; auto argsTuple = std::make_tuple(std::forward<Args>(args)...); // 使用 any 存储 tuple 的 const 引用,避免一次额外的拷贝。 // 但需要注意,如果 args 是临时对象,any 内的引用可能会悬空。 // 更安全的做法是存储 tuple 的值。这里为了效率,假设调用方保证参数在回调执行期间有效。 // 更稳健的实现可以存储 tuple 的 shared_ptr,或者对右值参数进行移动捕获。 const auto& argsTupleRef = argsTuple; std::any packedArgs = std::cref(argsTupleRef); for (const auto& wrapperPtr : snapshot) { if (!wrapperPtr->active.load(std::memory_order_acquire)) { ++result.skipped; continue; } // 类型匹配检查 if (!detail::isInvokableWith(*wrapperPtr, typeid(ArgsTuple))) { ++result.type_mismatches; continue; } try { wrapperPtr->invoker(packedArgs); // 调用! ++result.invoked; } catch (...) { ++result.failed; // 注入日志处理器,记录异常信息 if (logHandler_) { logHandler_(LogLevel::Error, "Callback threw an exception."); } } } return result; }

detail::isInvokableWith函数封装了之前提到的isTypeCompatible逻辑,并考虑了无参数事件等边界情况。

4.4unsubscribeclose:安全的资源回收

unsubscribe需要找到对应的回调包装器,并将其标记为非活动状态,然后等待可能正在执行的回调完成。

bool EventBus::unsubscribe(const std::string& eventName, CallbackId id) { std::unique_lock lock(mutex_); auto it = subscribers_.find(eventName); if (it == subscribers_.end()) { return false; } auto& callbacks = it->second; for (auto iter = callbacks.begin(); iter != callbacks.end(); ++iter) { if (auto wrapper = iter->lock()) { if (wrapper->id == id) { wrapper->active.store(false, std::memory_order_release); // 从列表中移除弱引用 callbacks.erase(iter); // 重要:等待该回调所有正在执行的调用结束。 // 这需要 wrapper 内部有一个计数器或条件变量。 detail::waitForCompletion(wrapper); return true; } } else { // 弱引用已失效,顺带清理 iter = callbacks.erase(iter); --iter; // 调整迭代器 } } return false; }

detail::waitForCompletion的实现依赖于包装器内部的同步机制。一个简单的方法是在CallbackWrapperBase中加入一个std::atomic<int> executionCounter和一个std::condition_variable completionCV。在invoker执行前递增计数器,执行后递减计数器并通知completionCVwaitForCompletion则等待计数器归零。

close()操作类似,但它会遍历所有事件的所有订阅者,将它们标记为非活动,清空订阅表,并等待所有正在执行的回调完成。之后,closed_标志被置为true,阻止新的订阅和发布。

5. 高级用法、性能考量与避坑指南

5.1 使用模式与最佳实践

  1. 事件命名:建议使用有意义的、分层的字符串,如"ui.button.click""network.response.received"。避免使用魔法字符串,可以考虑用constexpr常量或枚举类来定义事件名。
  2. 参数设计
    • 优先使用值类型和智能指针:对于小型数据(int,double,std::string等),直接传值或const&。对于大型数据,使用std::shared_ptr<const T>传递。这明确了所有权,并避免了不必要的拷贝。
    • 避免非const左值引用:事件总线不应被用作修改发布者数据的渠道。这违反了发布者与订阅者解耦的原则,且在多线程下极易出错。我的实现直接禁止了这类参数。
    • 考虑使用专用事件结构体:当参数超过3个时,定义一个struct EventData会更清晰,也便于后续扩展。
  3. 生命周期管理
    • 订阅者对象必须比事件总线或自身的订阅寿命更长。使用weak_ptr是黄金法则。
    • 在模块或对象析构时,务必调用unsubscribeunsubscribeAll。可以将CallbackId作为成员变量,在析构函数中清理。
  4. 错误处理publish返回的PublishResult包含了丰富的状态信息。在生产环境中,应该检查failed计数,并对异常进行日志记录。事件总线本身吞掉了回调异常,防止单个订阅者的错误导致整个事件流中断。

5.2 性能优化点

  1. 事件名查找std::unordered_map的查找是 O(1),但字符串哈希和比较仍有开销。对于性能极度敏感的场景,可以考虑使用std::string_view作为事件名的键类型,并要求用户提供生命周期至少与订阅期一样长的字符串字面量或静态字符串。我的实现为了通用性,仍使用std::string
  2. 参数打包与拷贝std::tuplestd::any的构造涉及拷贝或移动。对于大的参数,这个开销显著。使用std::shared_ptrstd::unique_ptr来传递大对象是推荐做法。发布时只拷贝一个指针,代价很小。
  3. 类型匹配开销:运行时的typeid比较和兼容性检查是有成本的。如果事件类型固定且已知,可以在订阅时通过typeid进行预检查,将不匹配的订阅拒绝在subscribe阶段。但这会限制灵活性。另一种思路是提供模板化的subscribe重载,在编译期确定事件类型,但这需要将事件名也模板化,改变了API设计。
  4. 内存分配:每次subscribepublish(打包参数)都可能涉及动态内存分配。可以使用内存池或小对象优化来减少分配次数。例如,为CallbackWrapper实现一个自定义的分配器。

5.3 常见问题与排查技巧

问题1:回调函数没有被调用。

  • 检查点1publish的返回值。查看PublishResult中的subscribers,type_mismatches,skipped字段。如果subscribers为0,说明没有订阅者;如果type_mismatches不为0,说明参数类型不匹配。
  • 检查点2:事件名拼写。大小写和空格是否完全一致?
  • 检查点3:生命周期。订阅是否发生在发布之后?订阅者对象是否在发布前已被销毁?(使用weak_ptr可以避免崩溃,但会导致静默失败)。
  • 检查点4:线程问题。是否在某个线程订阅,却在另一个尚未看到该订阅的线程发布?事件总线的订阅表是线程安全的,但存在“发布快照”的机制。如果发布线程在订阅线程将回调加入列表之前就取了快照,那么这次发布将看不到新订阅。这是设计上的最终一致性,通常不是问题,但需要了解。

问题2:程序崩溃,尤其是在取消订阅或关闭时。

  • 最常见原因:在回调函数内部,试图访问已经销毁的捕获变量(尤其是对象引用&this)。务必使用weak_ptr
  • 另一个原因:在回调函数内部,又调用了unsubscribeclose,并且事件总线在等待该回调完成,造成死锁。我的实现允许回调取消自身订阅,但需要仔细处理锁的顺序。确保回调中不要进行可能等待自身的操作。
  • 排查方法:开启事件总线的详细日志(如果支持),查看回调执行和取消订阅的顺序。使用 AddressSanitizer 或 Valgrind 检查内存错误。

问题3:性能瓶颈,发布事件耗时过长。

  • 分析工具:使用性能分析工具(如perf,VTune)定位热点。很可能是某个订阅者的回调函数本身执行太慢。
  • 优化策略
    • 审查回调函数逻辑,看是否能异步化或优化。
    • 检查是否订阅了过多不必要的回调。定期清理无用订阅。
    • 考虑对高频事件使用专门的事件总线实例,或者将事件分类,减少单个总线上的竞争。
    • 如果类型匹配是热点,考虑简化或禁用类型兼容性转换。

问题4:内存泄漏。

  • 原因:订阅后没有取消,特别是当订阅者对象已经销毁,但回调仍被事件总线持有(如果使用shared_ptr捕获this会导致对象无法释放)。
  • 解决方案:坚持使用weak_ptr模式。在对象析构函数中主动取消订阅。事件总线内部使用weak_ptr引用回调包装器,并定期清理失效的弱引用(如在unsubscribepublish遍历时)。

实现一个工业级强度的事件总线需要考虑许多边界情况,但核心思路是清晰的:类型擦除存储、线程安全的订阅管理、明确的资源生命周期。从简单的原型开始,逐步添加线程安全、生命周期管理、类型转换等功能,并辅以充分的单元测试(特别是多线程场景下的测试),是构建一个可靠事件总线系统的有效路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询