news 2026/2/14 8:53:19

ZLToolKit模块(三) NoticeCenter(事件广播)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZLToolKit模块(三) NoticeCenter(事件广播)

在 ZLMediaKit/ZLToolKit 的架构中,模块之间的解耦至关重要。NoticeCenter(通知中心)正是为此而生。它实现了一个观察者模式发布-订阅模式(Publish-Subscribe Pattern),充当了整个系统的“中枢神经”,负责将事件从发生地精准地投递给感兴趣的监听者。

1. 核心设计理念与原理

NoticeCenter的核心目标是:让事件的产生者(Emitter)不需要知道谁在监听,事件的消费者(Listener)也不需要知道谁发出的事件。

1.1 双层映射结构

为了高效管理成千上万的事件,NoticeCenter采用了双层 Hash Map的结构:

  1. 第一层(事件层)NoticeCenter内部维护一个unordered_map,Key 是事件名称(string),Value 是EventDispatcher(事件分发器)。
    • 作用:根据事件名快速找到对应的分发器。
  2. 第二层(监听层):每个EventDispatcher内部维护一个unordered_multimap,Key 是监听者标识(void* tag),Value 是回调函数(Any)
    • 作用:存储该特定事件的所有监听回调。

1.2 类型擦除与变参模板

C++ 是强类型语言,但通用的事件中心需要支持任意参数的回调。NoticeCenter结合了以下技术:

  • Any类型:类似于 C++17 的std::any,用于存储任意类型的函数对象(std::function),实现了回调函数的类型擦除。
  • 变参模板(Variadic Templates)emitEvent方法接受ArgsType &&...args,允许发射携带任意数量和类型参数的事件。

2. 核心类分析

2.1EventDispatcher(幕后英雄)

它是实际干活的类,对应“某一个特定事件”的管理者。

  • 职责:存储、删除、触发监听器。
  • 关键点
    • 防死锁机制:在emitEvent触发回调时,它会先拷贝一份监听列表,然后释放锁,再遍历拷贝的列表执行回调。
    • 为什么这么做?如果在回调函数中又调用了addListenerdelListener,如果不释放锁,就会导致死锁(Self-Deadlock)。
    • 中断机制:支持InterruptException。如果某个监听器抛出此异常,事件广播将立即停止,不再通知后续监听器。

2.2NoticeCenter(大管家)

它是单例模式(Singleton)的对外入口。

  • 职责:管理所有的EventDispatcher
  • 接口
    • addListener: 注册监听。
    • delListener: 移除监听(支持按 tag 移除单个,或按 tag 移除所有事件的监听)。
    • emitEvent: 发射事件。

2.3NoticeHelper(语法糖)

这是一个模板辅助类,配合宏NOTICE_EMIT使用。

  • 作用:简化代码书写,利用模板推导自动匹配参数类型,让调用看起来更像函数调用。

3. 类图概览

manages >
1
n
stores callbacks >
1
n
«Singleton»
NoticeCenter
-std::unordered_map _mapListener
-std::recursive_mutex _mtxListener
+static Instance()
+addListener(tag, event, func)
+delListener(tag, event)
+emitEvent(event, args...)
EventDispatcher
-std::unordered_multimap _mapListener
-std::recursive_mutex _mtxListener
+addListener(tag, func)
+delListener(tag, empty)
+emitEvent(safe, args...)
«Type Erasure»
Any
+set(value)
+get(safe)

4. 关键实现细节解析

4.1 线程安全与性能平衡

NoticeCenter在多线程环境下被高频使用,因此线程安全至关重要。

  • 使用了std::recursive_mutex(递归锁)。这允许同一个线程在持有锁的情况下再次获取锁,防止在复杂的嵌套调用(如回调中移除监听)中发生死锁。
  • Copy-On-Write (类似思想):如前所述,EventDispatcher::emitEvent中:
{std::lock_guard<std::recursive_mutex>lck(_mtxListener);copy=_mapListener;// 1. 临界区内只做拷贝,速度快}// 2. 临界区外执行回调,避免阻塞其他线程的添加/删除操作for(auto&pr:copy){...}

4.2tag指针的作用

addListener的第一个参数是void *tag

  • 通常用法:传入this指针。
  • 目的:当对象析构时,需要取消它注册的所有监听。通过tag(即对象的地址),NoticeCenter可以快速定位并移除该对象注册的所有回调,防止悬垂指针导致的崩溃。

5. 使用场景与示例

场景一:系统启动/停止通知

当服务器初始化完成或准备关闭时,通知各个模块。

监听者 (Listener):

classPlayer{public:Player(){// 关注 "SERVER_INIT" 事件// tag 传 this,方便析构时移除NoticeCenter::Instance().addListener(this,"SERVER_INIT",[](intport,conststring&ip){printf("Server started at %s:%d\n",ip.c_str(),port);});}~Player(){// 移除该对象的所有监听NoticeCenter::Instance().delListener(this);}};

触发者 (Emitter):

voidstartServer(){intport=8080;string ip="0.0.0.0";// 广播事件,参数自动透传NoticeCenter::Instance().emitEvent("SERVER_INIT",port,ip);}

场景二:使用宏简化调用

ZLToolKit 提供了NOTICE_EMIT宏,让代码更优雅。

// 定义事件参数签名// 这里的 void(int, string) 对应回调函数的签名NOTICE_EMIT(void(int,string),"SERVER_INIT",8080,"0.0.0.0");

场景三:中断事件传播

假设有一个鉴权事件,如果第一个监听者鉴权失败,不希望后续监听者继续处理。

NoticeCenter::Instance().addListener(this,"AUTH_USER",[](conststring&user){if(user=="admin"){// 鉴权通过}else{// 抛出中断异常,后续的回调将不会被执行throwEventDispatcher::InterruptException();}});

6. 总结

NoticeCenter是 ZLToolKit 中一个短小精悍但功能强大的组件。

  • 优点:解耦性强、线程安全、支持任意参数、支持事件中断。
  • 注意
    • 回调函数是在触发线程中执行的。如果回调处理耗时过长,会阻塞触发线程。对于耗时操作,建议在回调中抛转到线程池处理。
    • 务必在对象析构时调用delListener(this),否则会导致野指针回调崩溃。

通过理解NoticeCenter,你就能掌握 ZLMediaKit 中流媒体状态变化、Hook 机制等底层通信的脉络。

7. C++11

7.1 using和typedef的区别

using可以直接定义模板函数的别名:

template<typename T> using ptr = std::unique_ptr<T>; ptr<int> p(new int(10)); //正确 template<typename T> typedef std::unique_ptr<T> ptr; //错误 template<typename T> struct A { typedef std::unique_ptr<T> ptr; }; A<int>::ptr x(new int(10)); //正确

7.2 function_traits

function_traits是 ZLToolKit 中一个非常核心的 C++模板元编程(Template Metaprogramming)工具类。

它的主要作用是类型萃取(Type Extraction):在编译期解析出任何“可调用对象”(函数、函数指针、std::function、Lambda 表达式、仿函数、成员函数)的详细类型信息,例如返回值类型、参数个数、参数类型等。

template<typename T> struct function_traits; //普通函数, lambda表达式 template<typename Ret, typename... Args> struct function_traits<Ret(Args...)> //特化版本 { public: enum { arity = sizeof...(Args) }; typedef Ret function_type(Args...); typedef Ret return_type; using stl_function_type = std::function<function_type>; typedef Ret(*pointer)(Args...); template<size_t I> struct args { static_assert(I < arity, "index is out of range, index must less than sizeof Args"); using type = typename std::tuple_element<I, std::tuple<Args...> >::type; }; }; //函数指针 template<typename Ret, typename... Args> struct function_traits<Ret(*)(Args...)> : function_traits<Ret(Args...)>{}; //std::function template <typename Ret, typename... Args> struct function_traits<std::function<Ret(Args...)>> : function_traits<Ret(Args...)>{}; //member function #define FUNCTION_TRAITS(...) \ template <typename ReturnType, typename ClassType, typename... Args>\ struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{}; \ FUNCTION_TRAITS() FUNCTION_TRAITS(const) FUNCTION_TRAITS(volatile) FUNCTION_TRAITS(const volatile) //函数对象 template<typename Callable> struct function_traits : function_traits<decltype(&Callable::operator())>{};

可以通过这个类获取将任何函数类型转为stl的function类型。也可以活获取参数类型。
noticeCenter里有如下使用:

template<typename FUNC> void addListener(void *tag, FUNC &&func) { using funType = typename function_traits<typename std::remove_reference<FUNC>::type>::stl_function_type; std::shared_ptr<void> pListener(new funType(std::forward<FUNC>(func)), [](void *ptr) { funType *obj = (funType *) ptr; delete obj; }); std::lock_guard<std::recursive_mutex> lck(_mtxListener); _mapListener.emplace(tag, pListener); }

这里通过function_traitsstl_function_type得到stl的function类型,并构造了该类型的对象。

7.3 function

可以存储各种函数、函数指针、lambda 表达式等可调用对象。
例如,你可以使用std::function<int(int, float)>来存储一个参数类型为intfloat,返回值类型为int的可调用对象。

  • 将函数、函数指针、lambda 表达式等存储在一个变量中,方便传递和调用;
  • 在不确定可调用对象的具体类型的情况下,接受各种类型的可调用对象;
  • 在运行时动态绑定可调用对象;
  • 用于实现回调函数等;
  • 可以使用std::bind将函数或函数对象绑定到一个function变量中

例如:

#include <functional> int func(int x, float y) { return static_cast<int>(x + y); } int func2(int x, int y, int z) { return x + y + z; } struct FuncObj { int operator()(int x, float y) { return static_cast<int>(x * y); } }; int main() { std::function<int(int, float)> f1 = func; f1(1, 2.5f); // Output: 3 std::function<int(int, float)> f2 = [](int x, float y) { return static_cast<int>(x * y); }; f2(3, 0.5f); // Output: 1 std::function<int(int, float)> f3 = std::bind(func, std::placeholders::_1, std::placeholders::_2); std::cout << f1(1, 2.5f) << std::endl; // Output: 3 std::function<int(int, float)> f4 = std::bind(FuncObj(), std::placeholders::_1, std::placeholders::_2); std::cout << f4(3, 0.5f) << std::endl; // Output: 1 std::function<int(int)> f5 = std::bind(func2, 1, 2, std::placeholders::_1); std::cout << f5(3) << std::endl; // Output: 6 return 0; }

7.4 tuple_element

可以用于提取元组类型中某一位置的元素的类型。

using TupleType = std::tuple<int, float, double>; using ElementType = std::tuple_element<1, TupleType>::type; std::cout << typeid(ElementType).name() << std::endl; // Output: "float"
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/14 1:37:18

pymzML终极指南:Python质谱数据分析快速入门

pymzML终极指南&#xff1a;Python质谱数据分析快速入门 【免费下载链接】pymzML pymzML - an interface between Python and mzML Mass spectrometry Files 项目地址: https://gitcode.com/gh_mirrors/py/pymzML 在蛋白质组学和代谢组学研究中&#xff0c;质谱数据分析…

作者头像 李华
网站建设 2026/2/6 12:27:51

Qwen3-14B-AWQ:颠覆传统的大模型轻量化部署革命

技术迷局&#xff1a;当140亿参数遇上4-bit量化 【免费下载链接】Qwen3-14B-AWQ 项目地址: https://ai.gitcode.com/hf_mirrors/Qwen/Qwen3-14B-AWQ 2025年&#xff0c;大模型领域正面临一个关键转折点&#xff1a;企业渴求AI能力&#xff0c;却被算力成本压得喘不过气…

作者头像 李华
网站建设 2026/2/14 7:21:46

全域众链:从需求到落地,五大核心维度验证 AI + 实体的可行性

在 “AI 实体经济” 的赛道中&#xff0c;不少项目因脱离实际需求、缺乏落地能力沦为概念炒作&#xff0c;而全域众链凭借对市场痛点的精准把握、闭环化的模式设计、实用型的技术支撑&#xff0c;成为少数经得住实践检验的落地型生态。其可行性并非空谈&#xff0c;而是由市场…

作者头像 李华
网站建设 2026/2/7 13:06:55

45.限界上下文进阶(下)-微服务拆分6个原则-避免拆太细或拆不开附拆分决策树

45 限界上下文进阶(下):微服务拆分的 6 个原则(避免 “拆太细” 或 “拆不开”) 你好,欢迎来到第 45 讲。 在上一讲,我们确立了微服务拆分的“第一性原理”:以限界上下文为边界。这个原则,为我们从“战略”上,指明了拆分的方向。 但是,在从战略走向战术的落地过程…

作者头像 李华
网站建设 2026/2/8 6:56:11

bRPC深度架构剖析:从核心机制到百万级实战优化

bRPC深度架构剖析&#xff1a;从核心机制到百万级实战优化 【免费下载链接】brpc 项目地址: https://gitcode.com/gh_mirrors/br/brpc bRPC框架作为百度开源的高性能RPC解决方案&#xff0c;在分布式通信领域展现出了卓越的性能表现。本文将从技术架构深度剖析、核心机…

作者头像 李华
网站建设 2026/2/11 7:52:50

本地部署文件共享软件 Jirafeau 并实现外网访问

Jirafeau 是一款允许一键文件共享的开源软件&#xff0c;上传文件方式简单&#xff0c;为其提供一个唯一的链接。能够发送任何大小的文件&#xff0c;在浏览器预览并提供密码保护。本文将详细的介绍如何利用 Docker 在本地部署 Jirafeau 并结合路由侠实现外网访问本地部署的 Ji…

作者头像 李华