【译】eventpp CallbackList、EventDispatcher、EventQueue 类参考手册

2,339 阅读21分钟

eventpp 是一个 header-only 、简洁易用的 C++ 事件库,能为 C++ 程序添加类似于 Qt 信号槽系统的事件机制。本专栏以该库文档的中文翻译为主,内容已贡献到该库的代码仓

源码仓库:wqking/eventpp: Event Dispatcher and callback list for C++ (github.com)

CallbackList 类参考手册

目录

描述

CallbackList 是 eventpp 中最为核心、基础的类。EventDispatcher、EventQueue 都是以 CallbackList 类为基础开发的。

CallbackList 内部维护一个回调函数的列表。在 CallbackList 被调用时,其会逐个调用列表中的所有回调函数。可以将 CallbackList 看做 Qt 中的信号/槽系统,或某些 Windows API 中的回调函数指针(例如 ReadFileEx 中的 IpCompletionRoutine )。 回调函数可以是任何回调目标 —— 函数、函数指针、指向成员函数的指针、lambda 表达式、函数对象等。

API 参考

头文件

eventpp/callbacklist.h

模板参数

template <
  typename Prototype,
  typename Policies = DefaultPolicies
>
class CallbackList;

Prototype:回调函数原型。该参数应为 C++ 函数类型,如 void(int, std::string, const MyClass *)Policies:用于配置和扩展回调函数列表的规则。默认值为 DefaultPolicies 。详见 policy 文档

公共类型

Handle:由 appendprependinsert 函数返回的句柄类型。句柄可用于插入或移除一个回调函数。可以通过将句柄转为 bool 类型来检查其是否为空:句柄为空时值为 falseHandle 时可拷贝的。

成员函数

构造函数

CallbackList() noexcept;
CallbackList(const CallbackList & other);
CallbackList(CallbackList && other) noexcept;
CallbackList & operator = (const CallbackList & other);
CallbackList & operator = (CallbackList && other) noexcept;
void swap(CallbackList & other) noexcept;

CallbackList 可以被拷贝、move、赋值、move 赋值和交换。

empty

bool empty() const;

当回调列表为空时返回 true 。 提示:在多线程程序中,该函数的返回值无法保证确定就是列表的真实状态。可能会出现刚刚返回 true 之后列表马上变为非空的情况,反之亦然。

bool 转换运算符

operator bool() const;

若回调列表非空则返回 true。 借助该运算符,能够实现在条件语句中使用 CallbackList 实例

append

Handle append(const Callback & callback);

向回调函数列表中添加回调函数。 新的回调函数会被加在回调函数列表的末尾。 该函数返回一个代表回调函数的句柄。该句柄能够用于移除该回调函数,或在该回调函数前插入其他的回调函数。 如果append在回调函数列表执行的过程中被其他的回调函数调用,则新添加的回调函数一定不会在该回调函数列表执行的过程中被执行。 该函数的时间复杂度为 O(1) 。

prepend

Handle prepend(const Callback & callback);

向回调函数列表中添加回调函数。 回调函数将被加在回调函数列表的最前端。 该函数会返回一个代表回调函数的句柄(handler)。该句柄可被用于移除该回调函数,也可用于在该回调函数前插入其他回调函数。 如果 prepend 在回调函数列表执行的过程中被其他回调函数调用,则新添加的回调函数一定不会在该回调函数列表执行的过程中被执行。 该函数的时间复杂度为 O(1) 。

insert

Handle insert(const Callback & callback, const Handle & before);

callback 插入到回调列表中 before 前面的一个位置处。若找不到 before ,则 callback 会被加到回调列表的末尾。 该函数返回一个代表回调函数的句柄。该句柄可被用于移除该回调函数,也可用于在该回调函数前插入其他回调函数。 如果 insert 是在回调函数列表执行的过程中被其他回调函数调用的,则新添加的回调函数一定不会在该回调函数列表执行的过程中被执行。 该函数的时间复杂度为 O(1) 。

提示:该函数的调用者必须提前确定 before 是由 this 所指向的 CallbackList 调用的。如果不能确定,可以用 ownsHandle 函数来检查 before 是否属于 this CallbackList 。insert 函数仅能在 ownsHandle(before) 返回 true 的时候被调用,否则可能引发未定义的行为并带来诡异的 bug 。 需要确保只在 assert(ownsHandle(before))insert ,但出于性能方面的考量,发布的代码中没有包含相关的检查。

remove

bool remove(const Handle & handle);

从回调函数列表中移除 handle 指向的回调函数。 移除成功会返回 true ,找不到回调函数则会返回 false 。 该函数的时间复杂度为 O(1)

提示:handle 必须是由 this 指向的 CallbackList 所创建的。更多细节请查看 insert 中的“提示”部分

ownsHandle

bool ownsHandle(const Handle & handle) const;

handle 是由当前 CallbackList 创建时返回 true,否则返回 false 。 该函数时间复杂度为 O(N)

forEach

template <typename Func>
void forEach(Func && func) const;

对所有的回调函数使用 funcfunc 可以是下面两种原型的其中一种:

AnyReturnType func(const CallbackList::Handle &, const CallbackList::Callback &);
AnyReturnType func(const CallbackList:Callback &);

提示:func 可以安全地移除和添加新的回调函数

forEachIf

template <typename Func>
bool forEachIf(Func && func) const;

对所有的回调函数使用 funcfunc 必须返回一个 bool 值,若返回值为 false ,则 forEachIf 将会立即停止循环。 当所有回调函数都被触发后,或未找到 event 时,该函数会返回 true 。当 func 返回 false 时返回 false

调用运算符

void operator() (Args ...args) const;

触发回调函数列表中所有回调函数的运行。 回调函数会被用 args 参数作为参数调用。 回调函数会在 operator() 所在的线程中调用。

嵌套回调函数安全性

  1. 若一个回调函数在自身运行时,向回调函数列表中添加了新的回调函数,则新的回调函数不会在本次回调函数列表执行的过程中被执行。该行为是由一个 64 位无符号类型的计数器所保证的。如果在一次触发的过程中使该计数器溢出到 0 则会破坏上述的行为。
  2. 所有在回调函数列表运行过程中被移除的回调函数都不会被触发运行。
  3. 上面这几点在多线程情况下未必成立。也就是说,如果一个线程正在执行回调函数列表,如果另一个线程的函数向这个列表中添加或移除了函数,则被添加或移除的函数仍有可能在此次回调函数列表执行的过程中被执行。

时间复杂度

  • append:O(1)
  • prepend:O(1)
  • insert:O(1)
  • remove:O(1)

内部数据结构

CallbackList 使用双向链表管理回调函数。 每个节点都使用共享指针(shared pointer)连接。使用共享指针可以实现在迭代的过程中移除节点。

EventDispatcher (事件调度器)类参考手册

目录

描述

EventDispatcher 类似于 std::map<EventType, CallbackList>

EventDispatcher 维护一个 <EventType, CallbackList> 映射表。在进行事件调度时, EventDispatcher 会根据事件类型(EventType)查找并调用对应的回调列表(CallbackList) 。该调用过程是同步的,监听器会在 EventDispatcher::dispatch 被调用时触发。

API 参考

头文件

eventpp/eventdispatcher.h

模板参数

template <
    typename Event,
    typename Prototype,
    typename Policies = DefaultPolicies
>
class EventDispatcher;

Event:事件的类型。该类型用于识别事件,具有相同类型的事件就是同一个事件。事件类型必须是能在 std::mapstd::unordered_map 中用作 key 的类型,所以该类型应该要么可以使用 operator < 进行比较,要么派生自 std::hash

Prototype:监听器的原型。其应为 C++ 函数类型,例如 void(int, std::string, const MyClass *)

Policies:配置和扩展调度器的规则。默认值是 DefaultPolicies。 详情请阅读github.com/wqking/even… 文档。

公共类型

Handle:由 appendListenerprependListenerinsertListener 函数返回的 Handle (句柄)类型。可用于插入或移除一个监听器。可以通过将句柄转为 bool 类型来检查其是否为空:句柄为空时值为 falseHandle 时可拷贝的。

Callback:回调函数存储类型。

Event:事件类型。

成员函数

构造函数

EventDispatcher();
EventDispatcher(const EventDispatcher & other);
EventDispatcher(EventDispatcher && other) noexcept;
EventDispatcher & operator = (const EventDispatcher & other);
EventDispatcher & operator = (EventDispatcher && other) noexcept;
void swap(EventDispatcher & other) noexcept;

EventDispatcher 可以拷贝、move、赋值、move 赋值和交换

appendListener

Handler appendListener(const Event & event, const Callback & callback);

向调度器中添加用于监听 event 事件的回调函数 callback

监听器会被添加到监听器列表的末尾。

该函数会返回一个代表监听器的句柄。该句柄可用于移除该监听器,或在该监听器之前插入其他监听器。

appendListener 在一次事件调度的过程中被另一个监听器调用,则新的监听器不会在本次事件调度的过程中被触发。

如果同一个回调函数被添加了两次,则会调度器中出现两个相同的监听器。

该函数的时间复杂度为 O(1) + 在内部映射表中寻找事件的时间

prependListener

Handle prependListener(const Event & event, const Callback & callback);

向调度器中添加用于监听 event 事件的回调函数 callback

监听器会被添加到监听器列表的最前端。

该函数返回一个代表该监听器的句柄。该句柄可用于移除该监听器,也可以用于在该监听器前面再插入其他监听器。

prependListener 在一次事件调度的过程中被另一个监听器调用,则新的监听器不会在本次事件调度的过程中被触发。

该函数的时间复杂度为 O(1) + 在内部映射表中寻找事件的时间。

insertListener

Handle insertListener(const Event & event, const Callback & callback, const Handle before);

将回调函数 callback 插入到调度器中 before 监听器前面的一个位置,以监听 event 事件。如果没找到 beforecallback 会被添加到监听器列表的末尾。

该函数会返回一个代表该监听器的句柄。该句柄可用于移除监听器,也可以用于在该监听器前面再插入其他监听器。

insertListener 在一次事件调度的过程中被另一个监听器调用,则用该函数新插入的监听器不会在本次事件调度的过程中被触发。

该函数的时间复杂度为 O(1) + 在内部映射表中寻找事件的时间。

注意:调用者必须确保 before 句柄是由 this EventDispatcher 创建的。若不能确定,则可用 ownsHandle 来检查 before 句柄是否属于 this EventDispatcher。 insert 函数只能在 ownsHandle(before) 返回 true 时才能被调用,否则会出现未定义的行为,导致出现诡异的 bug。

insertListener 会在自身的回调列表中进行一次 assert(ownsHandle(before)) ,但出于性能方面的考量,在发布的代码中并不会进行检查。

removeListener

bool removeListener(const Event & event, const Handle handle);

移除调度器中 handle 所指向的监听 event 的监听器。

若监听器被成功移除,该函数返回 tue。若未找到对应监听器则返回 false。

该函数的时间复杂度为 O(1) + 在内部映射表中寻找事件的时间。

注意:handle 必须是由 this EventDispatcher 创建的。详细说明请查看 insertListener 中的注意部分。

hasAnyListener

bool hasAnyListener(const Event & event) const;

当存在与 event 对应的监听器时返回 ture ,否则返回 false 。

注意:在多线程中,该函数返回 true 时并不能保证 event 中一定存在监听器。回调列表可能在该函数返回 true 后就被清空了,反之亦然。该函数的时间复杂度为 O(1) + 在内部映射表中寻找事件的时间。

ownsHandle

bool ownsHandle(const Event & event, const Handle & handle) const;

handle 是由 EventDispatcher 为 event 创建的,返回 true,否则返回 false。

时间复杂度为 O(N)

forEach

template <typename Func>
void forEach(const Event & event, Func && func);

event 的所有监听器使用 func

func 可以是下面两个属性中的一个:

AnyReturnType func(const EventDispatcher::Handle &, const EventDispatcher::Callback &);
AnyReturnType func(const EventDispatcher::Callback &);

注意:func 可以安全地移除任何监听器或添加其他的监听器。

forEachIf

template <typename Func>
bool forEachIf(const Event & event, Func && func);

event 的所有监听器使用 funcfunc 必须返回一个 bool 值,若返回值为 false, forEachIf 将立即停止循环。

当所有监听器都被触发执行,或未找到 event 时返回 true 。当 func 返回 false 时返回 false

dispatch

void dispatch(Args ...args);

template <typename T>
void dispatch(T && first, Args ...args);

调度一个事件。dispatch 函数的参数是要被调度的事件类型。

所有的监听器都会被使用 args 参数调用。

该函数是同步的。所有监听器都会在调用 dispatch 的线程中被调用。

这两个重载函数略有区别,具体如何使用要根据 ArgumentPassingMode 策略而定。详情请阅读github.com/wqking/even… 文档。

嵌套监听器安全

  1. 若一个监听器在一次调度的过程中,向调度器中加入另一个有着相同事件类型的监听器,则新的监听器可以保证不会在本次时间调度的过程中被调用。这是由一个 64 位无符号整数类型的计数器保证的,若在一次调度的过程中该计数器值溢出到 0 则会破坏该规则,但该规则将继续处理子序列调度。
  2. 一次调度过程中移除的所有监听器都不会被触发。
  3. 上面的两点在多线程中不成立。若在一个线程正在触发回调列表的时候,其他线程添加或移除了一个回调函数,则被添加或移除的这个回调函数可能会在本次触发执行的过程中执行。

时间复杂度

此处讨论的时间复杂度是操作回调列表中的监听器的复杂度,n 是监听器的数量。并不包含搜索 std::map 中事件所消耗的时间,该部分的时间复杂度为 O(log n) 。

  • appendListener:O(1)
  • prependListener:O(1)
  • insertListener:O(1)
  • removeListener:O(1)

内部数据结构

EventDispatcher 使用 CallbackList 来管理监听器回调函数。

EventQueue (事件队列)类参考手册

目录

描述

EventQueue 在包含了 EventDispatcher 所有特性的基础上新增了事件队列特性。注意:EventQueue 并非派生自 EventDispatcher ,请勿尝试将 EventQueue 转换为 EventDispatcher 类型。

EventQueue 是异步的。事件会在调用 EventQueue::enqueue 时被缓存进 EventQueue 的事件队列,并在后续调用 EventQueue::process 时被调度。

EventQueue 相当于是 Qt 中的事件系统(QEvent),或 Windows API 中的信息处理(message processing)。

API 参考

头文件

eventpp/eventqueue.h

模板参数

template <
  typename Event,
  typename Prototype,
  typename Policies = DefaultPolicies
>
class EventQueue;

EventQueue 的模板参数与 EventDispatcher 的模板参数完全一致。详细信息请参阅 EventDispatcher 文档。

公共类型

QueuedEvent:存储在队列中的事件的数据类型。其声明的伪代码表示如下:

struct EventQueue::QueuedEvent
{
    EventType event;
    std::tuple<ArgTypes...> argument;
    
    // 获取事件
    EventType getEvent() const;
    
    // 获取索引为 N 的实参
    // 与 std::get<N>(queuedEvent.arguments) 相同
    template <std::size_t N>
    NthArgType getArgument() const;
};

event 是 EventQueue::Event , argumentsenqueue 中传递的实参。

成员函数

构造函数

EventQueue();
EventQueue(const EventQueue & other);
EventQueue(EventQueue && other) noexcept;
EventQueue & operator = (const EventQueue & other);
EventQueue & operator = (EventQueue && other) noexcept;

EventQueue 可以拷贝、移动、赋值和移动赋值

注意:已排入队列的事件无法被拷贝、移动、赋值和移动赋值,这些操作只会对监听器生效。这就意味着,已排入队列的事件不会在 EventQueue 被复制和赋值时复制。

enqueue

template <typename ...A>
void enqueue(A ...args);
​
template <typename T, typename ...A>
void enqueue(T && first, A ...args);

将一个事件加入事件队列。事件的类型包含在传给 enqueue 函数的实参中。

所有可拷贝实参都会被拷贝到内部的数据结构中。所有不可拷贝但可移动的实参都会被移动。

EventQueue 的参数必须满足可拷贝和可移动两项中的一项。

如果定义的参数是基类的引用,却传入了一个派生类的对象,那么就只会保存基类,派生部分则会丢失。这种情况下一般可以使用共享指针来满足相关需求。

如果参数是指针,那么 EventQueue 就只会存储指针。该指针所指向的对象必须在事件处理结束之前都是可用的。

enqueue 会唤醒所有由 waitwaitFor 阻塞的线程。

该函数的时间复杂度为 O(1) 。

这两个重载函数略有不同,具体的用法取决于 ArgumentPassingMode 策略。详情请阅读github.com/wqking/even… 文档。

process

bool process();

处理事件队列。所有事件队列中的事件都会被一次性调度,并从队列中移除。

若有事件被处理,该函数将返回 true 。返回 false 则代表未处理任何事件。

在哪个线程中调用了 process ,所有的监听器就会在哪个线程中执行。

process() 执行过程中新添加进队列的事件不会在当前的 process() 中被调度。

process() 能高效完成单线程事件处理,其会在当前线程中处理队列中的所有事件。若想在多个线程中高效处理事件,请使用 processOne()

注意:若 process() 被同时在多个线程中调用,事件队列中的事件也将只被处理一次。

processOne

bool processOne();

处理事件队列中的一个事件。该函数将会调度事件队列中的第一个事件,并将该事件移除队列。

若事件成功被处理,该函数返回 true ,否则返回 false 。

在哪个线程中调用了 processOne() ,监听器就会在哪个线程中执行。

在执行 processOne() 时被添加进队列的新事件将不会在当前的 processOne() 过程中被调度。

若有多个线程处理事件,processOne() 要比 process() 更高效,因为其能将事件处理分散到不同的线程中执行。但若只有一个事件处理线程,则 process() 更高效。

注意:若 processOne() 被同时在多个线程中调用,那么事件队列中的事件也只会被处理一次。

processIf

template <typename Predictor>
bool processIf(Predictor && predictor);

处理事件队列。在处理一个事件前,该事件将先被传给 predictor ,仅当 predictor 返回 true 时,该事件才会被处理。若返回 false ,则会跳过该事件继续处理后面的事件。被跳过的事件则会被继续保留在队列中。

predictor 是一个可调用对象(函数, lambda 表达式等),其接收的参数与 EventQueue::enqueue 接收的参数一致或不接收参数,返回值应为 bool 类型值。 eventpp 会正确地传递所有参数。若有事件被处理,该函数将返回 true 。返回 false 则代表未处理任何事件。

processIf 在下面这些场景中很有用:

  • 在特定的线程中处理特定的事件。例如在 GUI 应用中,UI 相关事件只应该在主线程中处理。则在该场景中, predictor 可以只对 UI 事件返回 true ,而对所有的非 UI 事件返回 false 。

processUntil

template <typename Predictor>
bool processUnitl(Predictor && predictor);

处理事件队列。在处理一个事件前,该事件将先被传给 predictor ,若其返回 true , processUnitl 将会立即停止事件处理并返回。若 predictor 返回 false ,则 processUntil 将继续处理事件。

predictor 是一个可调用对象(函数, lambda 表达式等),其接收的参数与 EventQueue::enqueue 接收的参数一致或不接收参数,返回值应为 bool 类型值。 eventpp 会正确地传递所有参数。若有事件被处理,该函数将返回 true 。返回 false 则代表未处理任何事件。

processUnitl 可通过限制事件处理时间来模拟“超时”。例如在游戏引擎中,一次事件处理时间要被限制在几毫秒之内,没处理完的事件需要留到下一个循环中进行处理。该需求就可以通过让 predictor 在超时的时候返回 true 来实现。

emptyQueue

bool emptyQueue() const;

在事件队列中没有事件时返回 true ,否则返回 false 。

注意:在多线程环境下,空状态可能在该函数返回后马上发生改变。

注意:不要用 while(!eventQueue.emptyQueue()) {} 的写法来写事件循环。因为编译器会内联代码,导致该循环永远检查不到空状态变化,进而造成死循环。安全的写法应该是 while(eventQueue.waitFor(std::chrono::nanoseconds(0)));

clearEvents

void clearEvents();

在不调度事件的情况下清空队列中的所有事件。

该函数可用于清空已排队事件中的引用(比如共享指针),以避免循环引用。

wait

void wait() const;

wait 将让当前线程持续阻塞,直至队列非空(加入了新的事件)。

注意:尽管 wait 在内部解决了假唤醒的问题,但并不能保证 wait 返回后队列非空。

wait 在使用一个线程处理事件队列时很有用,用法如下:

for(;;) {
    eventQueue.wait();
    eventQueue.process();
}

尽管上面的代码中不带 wait 也能正常运行,但那样做将浪费 CPU 性能。

waitFor

template <class Rep, class Period>
bool waitFor(const std::chrono::duration<Rep, Period> & duration) const;

等待不超过 duration 所指定的超时时间。

当队列非空时返回 true ,当超时时返回 false 。

waitFor 在当事件队列处理线程需要做其他条件检查时很有用,例如:

std::atomic<bool> shouldStop(false);
for(;;) {
    while(!eventQueue.waitFor(std::chrono::milliseconds(10) && !shouldStop.load());
    if(shouldStop.load()) {
        break;
    }
    
    eventQueue.process();
}

peekEvent

bool peekEvent(EventQueue::QueuedEvent * queuedEvent);

从事件队列中取出一个事件。事件将在 queuedEvent 中返回。

struct EventQueue::QueuedEvent
{
    TheEventType event;
    std::tuple<ArgumentTypes...> arguemnts;
};

queuedEvent 是一个 EventQueue::QueuedEvent 结构体。eventEventQueue::Eventargumentsenqueue 中传入的参数。

该函数在事件队列为空时返回 false ,事件成功取回时返回 true 。

在函数返回后,原事件不会被移除,而会仍然留在队列中。

注意:peekEvent 无法和不可拷贝的事件参数一起使用。若 peekEvent 在有不可拷贝参数时被调用,会导致编译失败。

takeEvent

bool takeEvent(EventQueue::QueuedEvent * queuedEvent);

从事件队列中取走一个事件,并将该事件从事件队列中移除。事件将在 queuedEvent 中返回。

该函数在事件队列为空时返回 false ,事件成功取回时返回 true 。

在函数返回后,原来的事件将被从事件队列中移除。

注意:takeEvent 可以和不可拷贝事件参数一起会用。

dispatch

void dispatch(const QueuedEvent & queuedEvent);

调度由 peekEventtakeEvent 返回的事件。

内部类 EventQueue::DisableQueueNotify

EventQueue::DisableQueueNotify 是一个 RAII 类,其用于临时防止事件队列唤醒等待的线程。当存在 DisableQueueNotify 对象时,调用 enqueue 不会唤醒任何由 wait 阻塞的线程。当离开 DisableQueueNotify 的作用域时,事件队列就重新可被唤醒了。若存在超过一个 DisableQueueNotify 对象,线程就只能够在所有的对象都被销毁后才能重新可被唤醒。DisableQueueNotify 在需要批量向事件队列中加入事件时,能够有效提升性能。例如,在游戏引擎的主循环中,可以在一帧的开始时创建 DisableQueueNotify ,紧接着向队列中添加一系列事件,然后在这一帧的末尾销毁 DisableQueueNotify ,开始处理这一帧中添加的所有事件。

DisableQueueNotify 的实例化,需要传入指向事件队列的指针。示例代码如下:

using EQ = eventpp::EventQueue<int, void()>;
EQ queue;
{
    EQ::DisableQueueNotify disableNotify(&queue);
    // 任何阻塞的线程都不会被下面的两行代码唤醒
    queue.enqueue(1);
    queue.enqueue(2);
}
// 任何阻塞的线程都会在此处被立即唤醒

// 因为这里没有 DisableQueueNotify 实例,因此任何阻塞线程都会被下面这行代码唤醒
queue.enqueue(3);

内部数据结构

EventQueue 使用三个 std::list 来管理事件队列。

第一个忙列表( busy list )维护已入列事件的所有节点。

第二个等待列表( idle list )维护所有等待中的节点。在一个事件完成调度并被从队列中移除后,EventQueue 将把没有用过的节点移入等待列表,而不是直接释放相应的内存。这能够改善性能并避免产生内存碎片。

第三个列表是在 process() 函数中使用的本地临时列表( local temporary list )。在一次处理的过程中,忙列表会被交换( swap )到临时列表,所有事件都是在临时列表中被调度的。在这之后,临时列表会被返回,并追加到等待列表中。