The Listener is besides the WaitSet one of the building blocks to realize a push approach to detect and react to certain events. The Listener offers the user the ability to attach objects with a corresponding event and callback. Whenever the object receives the specified event the corresponding callback is called in the Listener background thread as a reaction.
The two key differences to the WaitSet are that the Listener is event-driven and not event- and state-driven as the WaitSet and that the Listener creates a separate background thread in which the event-callbacks are executed in contrast to the WaitSet where the user has to call the event-callbacks explicitly.
- Condition Variable Used by an attached object to inform the Listener/WaitSet that an event has occurred.
- event is changing the state of an object.
- event driven a one time reaction which is caused directly by an event. Example: a new sample has been delivered to a subscriber.
- state predefined values to which the members of an object are set.
- state driven a repeating reaction which is continued as long as the state persists. Example: a subscriber has stored samples which were not inspected by the user.
The Listener is a variation of the reactor pattern and the usage should be similar to the WaitSet with a key difference - it should be event driven and not a mixture of event and state driven, depending on which event is attached, like in the WaitSet.
- Whenever an event occurs the corresponding callback should be called once as soon as possible.
- If an event occurs multiple times before the callback was called, the callback should be called once.
- If an event occurs while the callback is being executed the callback should be called again once.
- Thread-safety: attaching an event at any time from any thread.
- Thread-safety: detaching an event at any time from any thread.
- If the callback is currently running
detachEvent
blocks until the callback is finished. - After the
detachEvent
call the event-callback is not called anymore, even when the event was signalled whiledetachEvent
was running and the callback was not yet executed. - If the callback is detached from within the event-callback then
detachEvent
is non blocking. The event is detached right after thedetachEvent
call.
- If the callback is currently running
- One can attach at most one callback to a specific event of a specific object.
- Usually defined with an enum by the developer. One example is
SubscriberEvent::DATA_RECEIVED
. - Attaching a callback to an event where a callback has been already attached results in an error.
- Usually defined with an enum by the developer. One example is
- One can attach the same event to different objects at the same time.
- One can attach multiple different events to a single object
- When the Listener goes out of scope it detaches itself from every attached object via a callback provided by the attached object (like in the WaitSet).
- When the class which is attached to the Listener goes out of scope it detaches itself from the Listener via a callback provided by the Listener (like in the WaitSet).
+---------------------------+
| ConditionVariableData |
| - m_semaphore |
| - m_runtimeName |
| - m_toBeDestroyed |
| - m_activeNotifications |
+---------------------------+
| 1 | 1
| |
| 1 | n
+-----------------------------------------------+ +--------------------------------------------------+
| ConditionListener | | ConditionNotifier |
| ConditionListener(ConditionVariableData & ) | | ConditionNotifier(ConditionVariableData &, |
| | | uint64_t notificationIndex) |
| bool wasNotified() | | |
| void destroy() | | void notify() |
| NotificationVector_t wait() | | |
| NotificationVector_t timedWait() | | - m_condVarDataPtr : ConditionVariableData* |
| | | - m_notificationIndex |
| - m_condVarDataPtr : ConditionVariableData* | +--------------------------------------------------+
| - m_toBeDestroyed : std::atomic_bool | | 1
+-----------------------------------------------+ |
| 1 | n
| +--------------------------------+
| 1 | TriggerHandle |
+-------------------------------------------------+ | bool isValid() |
| Listener | | bool wasTriggered() |
| attachEvent(Triggerable, EventType, Callback) | | void trigger() |
| detachEvent(Triggerable, EventType) | | void reset() |
| | | void invalidate() |
| - m_events : Event_t[] | | void getUniqueId() |
| - m_thread : std::thread | | |
| - m_conditionListener : ConditionListener | | - m_conditionVariableDataPtr |
| | | - m_resetCallback |
| +----------------------------+ | | - m_uniqueTriggerId |
| | Event_t | | +--------------------------------+
| | void executeCallback() | | | 1
| | bool reset() | | |
| | bool init(...) | | | n
| | | | +-------------------------------------------------------+
| | - m_origin | | | Triggerable (e.g. Subscriber) |
| | - m_callback | | | |
| | - m_invalidationCallback | | | void invalidateTrigger(const uint64_t triggerId) |
| | - m_eventId | | | void enableEvent(TriggerHandle&&, const EventEnum ) |
| +----------------------------+ | | void enableEvent(TriggerHandle&&) |
+-------------------------------------------------+ | void disableEvent(const EventEnum) |
| void disableEvent() |
| |
| - m_triggerHandle : TriggerHandle |
+-------------------------------------------------------+
The Triggerable does not need to implement all enableEvent
,
disableEvent
variations, only the ones which are required by the use
case. The enableEvent
and disableEvent
, without the distincting EventEnum
,
can be used when there is only a single event which can be triggered.
- Creating Listener: a
ConditionVariableData
is created in the shared memory TheListener
uses theConditionListener
to wait for incoming events.
PoshRuntime
Listener |
| getMiddlewareConditionVariable : var |
| --------------------------------------> |
| ConditionListener(var) | ConditionListener
| ----------------------------------------+-----------------> |
| wait() : vector<uint64_t> | |
| ----------------------------------------+-----------------> |
- Attaching Triggerable Event (SubscriberEvent::DATA_RECEIVED) to Listener:
The Listener creates a TriggerHandle and provides it to the Triggerable (Subscriber)
via
enableEvent
so that the Triggerable owns the handle. Whenever the event occurs the Triggerable can use thetrigger()
method of the TriggerHandle to notify the Listener.
User Listener Triggerable
| attachEvent() | |
| ------------------> | TriggerHandle |
| | create | |
| | ---------> | |
| | enableEvent(std::move(TriggerHandle)) |
| | -----------+--------------------------------------> |
- Signal an event from Triggerable:
TriggerHandle::trigger()
is called, the Listener is returning from theConditionListener.wait()
call and retrieves a list of all the signal notifications. The corresponding event-callbacks are called.
Triggerable TriggerHandle ConditionNotifier ConditionListener Listener Event_t
| trigger() | | | wait() : notificationIds | |
| -------------> | notify() | | <------------------------- | |
| | -------------> | .... unblocks .... | blocks | exeuteCallback() |
| | | | | ------------------------> |
| | | | | m_events[notificationId] |
- Triggerable goes out of scope: The TriggerHandle is a member of the
Triggerable, therefore the d'tor of the TriggerHandle is called which removes
the trigger from the Listener via the
resetCallback
Triggerable TriggerHandle Listener Event_t
| ~TriggerHandle | | |
| ----------------> | removeTrigger() | |
| | ----------------> | reset() |
| | via resetCallback | ------------> |
- Listener goes out of scope: The d'tor of the
Event_t
invalidates the Trigger inside the Triggerable via theinvalidationCallback
Listener Event_t Triggerable
| ~Event_t() | |
| -----------> | invalidateTrigger() |
| | -----------------------> |
| | via invalidationCallback |
- Problem: The Triggerable should be able to notify a Listener/WaitSet without having any knowledge about those class so that circular dependencies can be prevented. Furthermore, the Triggerable must be able to remove its attached events when it goes out of scope.
- Solution: The dependency inversion principle, create an abstraction which
is known by both, the TriggerHandle. Created by the Listener/WaitSet and
attached to the Triggerable so that it can notify the Listener/WaitSet via
the underlying
ConditionNotifier
withTriggerHandle::notify()
. The cleanup task is performed by them_resetCallback
so that the Triggerable has no dependencies to any Notifyable.
The ConditionListener
and ConditionNotifier
are two different interfaces to
the same class which state is stored in the ConditionVariableData
class. The
intention of the separation is to provide one side (e.g. Triggerable) only
an API to notify the Notifyable (e.g. Listener) whereas the Notifyable can only
wait on events. So the contract is reflected in the design.
- Problem: Since the Listener reacts on events and not states it requires the knowledge by whom it was notified.
- Solution:
- Every TriggerHandle has a unique id which is used as index in
ConditionNotifier
. - When
ConditionNotifier::notify
is called the Listener is informed via theNotificationVector_t
return value fromConditionListener::wait()
which index notified him. Hence it is the same as the unique id of the TriggerHandle the Listener knows which Triggerable notified him.
- Every TriggerHandle has a unique id which is used as index in
The Listener must be able to attach, detach events concurrently. Additionally, it supports that a callback can attach or detach further events or detach its corresponding event concurrently. Furthermore, the Listener supports that a callback is called concurrently while events are being attached/detached.
To realize this we created the Event_t
abstraction which is stored in an
array called m_events
. If we would like to attach or detach an event we either
initialize Event_t::init()
or reset Event_t::reset()
the corresponding entry
in the m_events
array. The array has the advantage that the data structure
itself never changes during runtime therefore it does not have to be thread-safe.
The thread-safety must then be ensured by the Event_t
class itself. Since
every concurrent action is contained in Event_t
we can use
concurrent::smart_lock
in combination with a std::recursive_mutex
to
guarantee the thread-safe access.
- Concurrent attach/detach event and callback execution, ensured by
thread-safe
Event_t
. - Detaching itself from within a callback is ensured via the
std::recursive_mutex
. - Attaching/detaching arbitrary events from within a callback is ensured by
securing every
Event_t
object inm_events
with aconcurrent::smart_lock
. This would not be possible if the data structure itself had to be thread-safe.
Since the event contains everything which is required to handle events it has
the responsibility to ensure the life-time of the TriggerHandle. This is done
by the m_invalidationCallback
which is called in Event_t::reset()
to
invalidate the TriggerHandle in the corresponding Triggerable. This is either
done when an event is detached or the Listener goes out of scope.
The Triggerable is a set of classes of which events can be attached to the Listener.
It is possible to either attach a specific event of a class to the Listener or attach the class without providing an event.
The basic idea is that the Listener creates a TriggerHandle whenever an event is attached and provides that TriggerHandle to the corresponding Triggerable. The Triggerable uses then the TriggerHandle to notify the Listener about events.
Every Triggerable requires:
- The private methods:
void enableEvent(iox::popo::TriggerHandle&& triggerHandle) noexcept;
void disableEvent() noexcept;
void invalidateTrigger(const uint64_t uniqueTriggerId) noexcept;
Every Triggerable requires:
- An
enum class
which usesiox::popo::EventEnumIdentifier
as underlying type.
enum class EventEnum : iox::popo::EventEnumIdentifier {
EVENT_IDENTIFIER,
ANOTHER_EVENT_IDENTIFIER,
};
- The private methods:
void enableEvent(iox::popo::TriggerHandle&& triggerHandle, const EventEnum event) noexcept;
void disableEvent(const EventEnum event) noexcept;
void invalidateTrigger(const uint64_t uniqueTriggerId) noexcept;
The methods above are used by the Listener to transfer the ownership of the TriggerHandle to the Triggerable. The Triggerable should have one TriggerHandle member for every attachable event/state. The TriggerHandle is then used to notify the Listener by the Triggerable that a certain event has occurred.
- It must be friend with
iox::popo::NotificationAttorney
. It is possible to provide public access to the previous methods but then the user has the ability to call methods which should only used by the Listener.