event_loop, event_loop_n, event_loop_th
#include "dasynq.h" namespace dasynq { // generic event loop template <typename T_Mutex> class event_loop; // non-threadsafe event loop typedef event_loop<null_mutex> event_loop_n; // threadsafe event loop typedef event_loop<std::mutex> event_loop_th; }
Brief: The event_loop templates defines an event loop multiplexor which can be used to listen or poll for events. A number of different types of event are handled, including I/O readiness on file descriptors, POSIX signals, child process status changes, and timer expiry. To receive an event the client first registers a watcher for the event and event source they are interested in, and then calls the run or poll methods of the event loop. The callback function of the watcher will be called during event processing to respond to the event.
Template parameters
- T_Mutex — specifies a mutex class to be used wherever a mutex is required. The
interface should conform to that of std::mutex, with lock,
unlock and try_lock functions.
Note: it is required that the functions of T_Mutex do not throw exceptions if used properly. Technically std::mutex may throw an exception if a system-level error occurs, but realistically this should only occur in the case of application error (memory corruption, double lock etc). If it does, it will cause the client application to terminate.
Members
Constructors
- event_loop() - default constructor; may throw std::bad_alloc or std::system_error. See details section.
- event_loop(delayed_init) noexcept - constructor for delayed initialisation. The event loop instance must be initialised by calling the init function before other functions are called or watchers are registered.
Types
- loop_traits_t — loop traits.
- mutex_t — alias for T_Mutex
- fd_watcher — a watcher type for watching file descriptor events
- fd_watcher_impl [template] — a template for implementing fd_watcher subclasses
- bidi_fd_watcher — a watcher type for watching file descriptor events, supporting both input and output events simultaneously
- bidi_fd_watcher_impl [template] — a template for implementing bidi_fd_watcher subclasses
- signal_watcher — a watcher type for watching POSIX signal reception
- signal_watcher_impl [template] — a template for implementing signal_watcher subclasses
- child_proc_watcher — a watcher type for watching child process status changes
- child_proc_watcher_impl [template] — a template for implementing child_proc_watcher subclasses
- timer — a timer implementation and watcher for timer expiry
- timer_impl [template] — a template for implementing timer subclasses
Functions
- void init() - initialise an event loop that has not yet been initialised (i.e. which was constructed with the delayed_init tag and for which init has not yet been called. Must not be called on an initialised event loop instance. May throw std::bad_alloc or std::system_error.
- void run(int limit = -1) noexcept - wait while there are no events pending; queue and process pending events, up to the number specified by limit (no limit if the default value of -1 is specified).
- void poll(int limit = -1) noexcept - queue and process any currently pending events, up to the number specified by limit (no limit if the default value of -1 is specified).
- void get_time(timespec &ts, clock_type clock, bool force_update = false) noexcept
void get_time(time_val &tv, clock_type clock, bool force_update = false) noexcept
— get the current time for the specified clock. The clock times may be cached for performance reasons; specify force_update as true to avoid using a cached value (in general this should not be necessary).
Destructor
- ~event_loop() — the destructor does nothing other than deallocate internal resources. Note in particular that watchers do not have their watch_removed functions called.
Details and usage
Instances can be constructed using the default constructor (which performs initialisation) or the delayed
initialisation constructor. If the delayed initialisation constructor is used, the instance remains uninitialised
until the init function is called. Only one instance can be active (initialised) at a time
in a process; libraries that require event loop functionality are encouraged to allow an
event_loop reference to be passed as a parameter from the application. Construction of a
second event loop while a first is still active has undefined behaviour.
Note: This limitation is a consequence of limitations in the backends (signals require handlers to
be setup on some systems, and child process status requires watching for a signal).
Watchers for events of interest should be registered with the event loop, and their callbacks will be notified when the events are processed. Event processing occurs only during execution of the run() and poll() functions. Watcher callbacks can return a rearm value to perform various actions, including to re-enable the watcher in order to detect future events.
Most operations involving event watchers, such as registration, removing, enabling, and disabling, are performed via the watcher instance; consult the documentation for the various watcher types. In general, watcher operations other than registration can not fail and will not throw exceptions. This means that critical watchers can be registered early and then enabled when needed; resource issues will be identified early, at the time of registration.
Event loop initialisation
Because the event loop may need to mask certain signals for the whole process, and because there is no straight-forward way to do this once multiple threads have been created, the event loop must be initialised in the initial application thread — or alternatively, these signals must be blocked by the application on the initial thread (the signals in question are SIGCHLD and SIGALRM). An event loop with a null_mutex mutex type will use sigprocmask to mask signals and therefore has (according to POSIX) unpredictable behaviour in multi-threaded applications; an event loop with any other mutex type will instead use pthread_sigmask, which requires linking against the pthreads library. It is therefore not possible to use a single-threaded event loop in a multi-threaded program, even if it would be used only from a single thread.
Watcher constraints and state cycle
Watchers are initially in a waiting state once enabled. An enabled watcher for which a watched event is detected (during the execution of the run or poll functions) is disabled and becomes queued for notification until its callback is run; during execution of the callback, the watcher is in the processing state.
A watcher must not be re-enabled while in the queued or processing state, unless the
event loop will not be polled (via run or poll) before the associated
event is processed. For a client program which polls the event loop from multiple threads, this (roughly) means that
watchers should not be enabled, once they have become disabled, except by returning a suitable
rearm code from the callback.
Note: Technically, the requirement is that polling must not be performed if an event may be detected
via an enabled watcher that is not in the waiting state.
Watchers may not be registered with more than one event loop at the same time (nor with the same loop more than once at the same time).
Event batching
Events are queued and processed in batches. Watchers can be assigned a priority value and processing of queued events occurs in priority order; however, incoming events currently do not cause additional watchers to be queued while in the processing phase, meaning that events may be processed slightly out of priority order. To mitigate this a processing batch size limit can be specified as an argument to the run or poll function. A smaller batch limit gives more accurate priority ordering, at a cost to throughput.
Note that watchers which re-queue immediately after processing (by returning rearm::REQUEUE from the callback function, or due to emulation mode of an fd_watcher watching readiness state of a regular file) would cause an unlimited processing cycle if no specific batch limit was specified; to mitigate this, the limit is reduced to the number of currently queued events, which should generally prevent a watcher from being processed more than once. However, high-priority watchers which requeue themselves may still be processed more than once since they will continue to jump to the start of the queue, and this may cause starvation for lower priority watchers.
Removal of watchers
Watchers can be requested to be removed from an event_loop, however removal does not necessarily occur immediately (except for non-threadsafe event loops; for threadsafe event loops, any delay is due to the watcher currently having its callback executed). All watcher types have a watch_removed function:
virtual void watch_removed() noexcept
This function can be overridden by watch implementations and will be called once the watcher has been removed; at this point it is safe to delete the watcher. Naturally watcher instances must not end their lifetime while they are registered to an event loop.
Forking processes
The event loop state in the child process after forking is unspecified. The event loop should be destroyed and reconstructed in the child before it is used again.
Signals
The event loop may internally use certain signals, including SIGCHLD and SIGALRM. Blocking or installing handlers for these signals may interfere with the function of the event loop. Other signals should be safe for application use, although these can be handled easily using the event loop's support via signal watchers.
loop_traits_t (dasynq::event_loop<T>::loop_traits_t)
// Member of dasynq::event_loop typedef unspecified loop_traits_t;
Brief: this traits class provides details of the supported functionality of the event loop.
Data members
- bool has_separate_rw_fd_watches [static constexpr] — indicates whether the loop requires read and write watches for the same file descriptor to be added separately.
- bool full_timer_support [static constexpr] — if false, the event loop might not differentiate between the system and monotonic clock, and timers against the system clock might not expire at the correct time if the system time is altered after the timer is set.