文章目录
1. 概述
本文翻译自:design/listener.md
除了WaitSet之外,Listener也是实现推送方法检测和响应某些事件的构建块之一。Listener为用户提供了将对象与相应的事件和回调关联的能力。每当对象接收到指定事件时,Listener后台线程会作为反应调用相应的回调。
与WaitSet的两个关键区别在于,Listener是事件驱动的,而不是事件和状态驱动的,并且Listener创建了一个单独的后台线程来执行事件回调,而在WaitSet中用户必须显式调用事件回调。
2. 术语
- Condition Variable
附加对象用于通知Listener/WaitSet事件已发生。 - event 改变对象状态的事件。
- event driven 由事件直接引发的一次性反应。
例如:新样本已被传递给订阅者。 - state 预定义的对象成员设置值。
- state driven 持续反应,只要状态持续存在。
例如:订阅者存储了用户未检查的样本。
3. 设计
Listener是Reactor反应器模式的一种变体,其使用方法应类似于WaitSet,但有一个关键区别——它应是事件驱动的,而不是像WaitSet那样根据附加的事件混合事件和状态驱动。
3.1 需求
- 每当事件发生时,应尽快调用相应的回调once。
- 如果在调用回调之前事件发生多次,应once调用回调。
- 如果在执行回调时事件发生,应once再次调用回调。
- 线程安全:可以随时从任何线程附加事件。
- 线程安全:可以随时从任何线程分离事件。
- 如果回调当前正在运行,
detachEvent
将阻塞直到回调完成。 - 在
detachEvent
调用后,不再调用事件回调,即使事件在detachEvent
运行期间被信号通知并且回调尚未执行。 - 如果从事件回调中分离回调,则
detachEvent
是非阻塞的。事件在detachEvent
调用后立即分离。
- 如果回调当前正在运行,
- 可以为特定对象的特定事件附加最多一个回调。
- 通常由开发人员定义的枚举。例如
SubscriberEvent::DATA_RECEIVED
。 - 附加到已经附加回调的事件会导致错误。
- 通常由开发人员定义的枚举。例如
- 可以同时为不同对象附加相同事件。
- 可以为单个对象附加多个不同事件。
- 当Listener超出作用域时,通过附加对象提供的回调将其从每个附加对象分离(如WaitSet中)。
- 当附加到Listener的类超出作用域时,通过Listener提供的回调将其从Listener分离(如WaitSet中)。
3.2 解决方案
3.3.1 类图
3.3.2 类交互
- 创建Listener: 在共享内存中创建
ConditionVariableData
。Listener使用ConditionListener
等待传入的事件。
PoshRuntime
Listener |
| getMiddlewareConditionVariable : var |
| --------------------------------------> |
| ConditionListener(var) | ConditionListener
| ----------------------------------------+-----------------> |
| wait() : vector<uint64_t> | |
| ----------------------------------------+-----------------> |
- 将Triggerable事件(SubscriberEvent::DATA_RECEIVED)附加到Listener:
Listener创建TriggerHandle并通过enableEvent
将其提供给Triggerable(Subscriber),使Triggerable拥有该句柄。每当事件发生时,Triggerable可以使用TriggerHandle的trigger()
方法通知Listener。
User Listener Triggerable
| attachEvent() | |
| ------------------> | TriggerHandle |
| | create | |
| | ---------> | |
| | enableEvent(std::move(TriggerHandle)) |
| | -----------+--------------------------------------> |
- 从Triggerable信号事件: 调用
TriggerHandle::trigger()
,Listener从ConditionListener.wait()
调用返回,并检索所有信号通知的列表。调用相应的事件回调。
Triggerable TriggerHandle ConditionNotifier ConditionListener Listener Event_t
| trigger() | | | wait() : notificationIds | |
| -------------> | notify() | | <------------------------- | |
| | -------------> | .... unblocks .... | blocks | exeuteCallback() |
| | | | | ------------------------> |
| | | | | m_events[notificationId] |
- Triggerable超出作用域: TriggerHandle是Triggerable的成员,因此调用TriggerHandle的析构函数,通过
resetCallback
从Listener中移除触发器。
Triggerable TriggerHandle Listener Event_t
| ~TriggerHandle | | |
| ----------------> | removeTrigger() | |
| | ----------------> | reset() |
| | via resetCallback | ------------> |
- Listener超出作用域:
Event_t
的析构函数通过invalidationCallback
使Triggerable中的Trigger无效。
Triggerable TriggerHandle Listener Event_t
| ~TriggerHandle | | |
| ----------------> | removeTrigger() | |
| | ----------------> | reset() |
| | via resetCallback | ------------> |
3.3.3 TriggerHandle
- 问题: Triggerable应能够在没有任何有关这些类的知识的情况下通知Listener/WaitSet,以防止循环依赖。此外,Triggerable必须能够在超出作用域时移除其附加的事件。
- 解决方案: 依赖反转原则,创建一个两者都知道的抽象,即TriggerHandle。由Listener/WaitSet创建并附加到Triggerable,以便它可以通过底层的
ConditionNotifier
用TriggerHandle::notify()
通知Listener/WaitSet。清理任务由m_resetCallback
执行,因此Triggerable对任何Notifyable没有依赖关系。
3.3.4 Condition Variable
ConditionListener
和ConditionNotifier
是同一类的两个不同接口,其状态存储在ConditionVariableData
类中。分离的目的是为一方(例如Triggerable)提供仅用于通知Notifyable(例如Listener)的API,而Notifyable只能等待事件。因此合同在设计中得以体现。
- 问题: 由于Listener对事件而不是状态做出反应,因此需要知道是谁通知了它。
- 解决方案:
- 每个TriggerHandle都有一个唯一ID,作为
ConditionNotifier
中的索引。 - 当调用
ConditionNotifier::notify
时,Listener通过ConditionListener::wait()
返回的NotificationVector_t
返回值获知哪个索引通知了它。因此,与TriggerHandle的唯一ID相同,Listener知道是哪个Triggerable通知了它。
- 每个TriggerHandle都有一个唯一ID,作为
3.3.5 Event_t
并发
Listener必须能够并发地附加和分离事件。此外,它支持在回调中附加或分离进一步的事件或分离其相应的事件。此外,Listener支持在附加/分离事件时并发调用回调。
为此,我们创建了Event_t
抽象,它存储在称为m_events
的数组中。如果我们想要附加或分离事件,我们要么初始化`Event_t
::init(),要么重置
Event_t::reset()`数组中的相应条目。数组的优点是数据结构本身在运行时不会改变,因此不必是线程安全的。
线程安全必须由Event_t
类本身确保。由于每个并发操作都包含在Event_t
中,我们可以使用concurrent::smart_lock
结合std::recursive_mutex
来保证线程安全访问。
- 并发附加/分离事件和回调执行,通过线程安全的
Event_t
确保。 - 通过
std::recursive_mutex
从回调中分离自身。 - 通过使用
concurrent::smart_lock
保护m_events
中的每个Event_t
对象,确保在回调中附加/分离任意事件。如果数据结构本身必须是线程安全的,这是不可能的。
生命周期
由于事件包含处理事件所需的一切,因此它有责任确保TriggerHandle的生命周期。这是通过在Event_t::reset()
中调用m_invalidationCallback
使相应Triggerable中的TriggerHandle无效来实现的。这在事件分离或Listener超出作用域时完成。
3.3.6 Triggerable(例如Subscriber)
Triggerable是一组类,这些类的事件可以附加到Listener。
可以将特定类的特定事件附加到Listener,或者不提供事件地附加类。
基本思路是,每当附加事件时,Listener会创建一个TriggerHandle并将其提供给相应的Triggerable。Triggerable然后使用TriggerHandle通知Listener事件的发生。
带单个事件的Triggerable
每个Triggerable需要:
- 私有方法:
void enableEvent(iox::popo::TriggerHandle&& triggerHandle) noexcept;
void disableEvent() noexcept;
void invalidateTrigger(const uint64_t uniqueTriggerId) noexcept;
带多个事件的Triggerable
每个Triggerable需要:
- 使用
iox::popo::EventEnumIdentifier
作为底层类型的enum class
。
enum class EventEnum : iox::popo::EventEnumIdentifier {
EVENT_IDENTIFIER,
ANOTHER_EVENT_IDENTIFIER,
};
- 私有方法:
void enableEvent(iox::popo::TriggerHandle&& triggerHandle, const EventEnum event) noexcept;
void disableEvent(const EventEnum event) noexcept;
void invalidateTrigger(const uint64_t uniqueTriggerId) noexcept;
上述方法由Listener使用,以将TriggerHandle的所有权转移到Triggerable。Triggerable应为每个可附加的事件/状态有一个TriggerHandle成员。
TriggerHandle然后用于通知Listener某个事件已发生。
- 它必须与
iox::popo::NotificationAttorney
是朋友。可以提供对先前方法的公共访问,但这样用户就可以调用仅应由Listener使用的方法。