1
0
mirror of https://github.com/wqking/eventpp.git synced 2024-12-27 16:41:11 +08:00
eventpp/doc/cn/eventqueue.md

12 KiB
Raw Blame History

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 策略。详情请阅读https://github.com/wqking/eventpp/blob/master/doc/policies.md 文档。

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::Event argumentsenqueue 中传入的参数。

该函数在事件队列为空时返回 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 )到临时列表,所有事件都是在临时列表中被调度的。在这之后,临时列表会被返回,并追加到等待列表中。