Introduction
Earlier, I
explained four foundational event handler patterns in C++—from
basic callbacks to observer systems (https://a5w1h.blogspot.com/2025/06/event-handlers-in-c-journey-from.html). Today,
we level up: how do you execute actions not just when an event
occurs, but precisely when you need them—even seconds, minutes, or hours later?
Imagine:
·
A security system that delays alarm triggers by 30
seconds to allow authorized deactivation.
·
An e-commerce platform that schedules abandoned-cart
reminders 2 hours after a user leaves.
·
A robotics controller that sequences movements with
millisecond-precision pauses.
These aren’t
theoretical—they’re real-world problems solved by decoupling event detection
from action timing. The CTimedEvent class embodies this
philosophy.
The CTimedEvent
class provides a
thread-safe event system where subscribers can attach callbacks to be executed
either immediately or with a specified delay when an event is triggered. This
design is common in applications like game engines, IoT systems, or UI
frameworks where actions need to respond to events synchronously or
asynchronously.
void subscribe(Callback callback) {
std::lock_guard<std::mutex> lock(mutex_);
immediate_callbacks_.push_back(std::move(callback));
}
void subscribe_with_delay(Callback callback, unsigned int delay_ms) {
std::lock_guard<std::mutex> lock(mutex_);
delayed_callbacks_.push_back({std::move(callback), delay_ms});
}
void trigger(Args... args) {
// Process immediate callbacks
{
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& cb : immediate_callbacks_) {
if (cb) cb(args...);
}
}
// Process delayed callbacks
{
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& tcb : delayed_callbacks_) {
if (tcb.func) {
launch_delayed(tcb.func, tcb.delay_ms, args...);
}
}
}
}
You may find the source
code in github: https://github.com/happytong/EventTemplates/blob/main/CTimedEvent.h.
Key Components &
Design Principles
1.
Observer Pattern:
o Purpose: Decouples event
sources from subscribers.
o Implementation:
§ subscribe()
: Registers immediate
callbacks.
§ subscribe_with_delay()
: Registers delayed
callbacks.
§ trigger()
: Notifies all
subscribers when an event occurs.
o Benefit: Flexible, dynamic
subscription without modifying the event source.
2.
RAII (Resource Acquisition Is Initialization):
o std::lock_guard
in subscribe()
, subscribe_with_delay()
, and trigger()
automatically
locks/unlocks mutex_
to ensure thread safety.
3.
Concurrency & Thread Safety:
o Mutex (mutex_
): Protects shared data
(immediate_callbacks_
, delayed_callbacks_
) from race conditions.
o Detached Threads: Delayed callbacks run
in separate threads to avoid blocking the main thread.
4.
Template Flexibility:
o template
<typename... Args>
allows the class to handle events with any argument
signature (e.g., CTimedEvent<int, string>
).
Workflow Explained
Here’s how the system
processes a trigger()
call:
1.
Immediate Callbacks:
o Executed synchronously
in the triggering
thread.
o Example: Logging,
real-time state updates.
2.
Delayed Callbacks:
o Each callback runs in
a separate
detached thread.
o The thread:
1.
Binds arguments to the callback.
2.
Sleeps via delay(delay_ms)
.
3.
Executes the callback.
o Example: Scheduled
tasks, debouncing user input.
Critical Functions
1.
delay(int nMs)
:
o Uses nanosleep()
to pause
execution for nMs
milliseconds. You may replace delay()
with std::this_thread::sleep_for()
.
o Handles EINTR
(interrupted
system calls) by retrying.
2.
launch_delayed()
:
o Binds arguments to the
callback using std::bind
.
o Spawns a detached
thread to manage the delay and execution.
3.
trigger(Args... args)
:
o Processes immediate
callbacks under lock.
o Launches threads for
delayed callbacks (also under lock).
Real-World Use Cases
1.
Game Development:
o Immediate: Update player health
on collision.
o Delayed: Respawn enemies after
5 seconds.
2.
UI Applications:
o Immediate: Refresh UI on data
change.
o Delayed: Auto-save form data
after 2 seconds of inactivity.
3.
IoT Systems:
o Immediate: Alert on critical
sensor readings.
o Delayed: Turn off lights after
10 minutes of no motion.
Potential Pitfalls
& Improvements
1.
Resource Exhaustion:
o Risk: Many delayed
callbacks → excessive threads.
o Fix: Use a thread pool +
priority queue (e.g., a scheduler thread).
2.
Argument Lifetime:
o Risk: Arguments referenced
in delayed callbacks may be destroyed.
o Fix: Use shared_ptr
or copy arguments
by value.
3.
Exception Handling:
o Risk: Exceptions in
callbacks terminate detached threads.
o Fix: Wrap callbacks
in try/catch
.
4.
Scalability:
o Risk: Lock contention in
high-frequency events.
o Fix: Copy callbacks under
lock, then execute without lock:
std::vector<Callback> tmp;
{
std::lock_guard<std::mutex> lock(mutex_);
tmp = immediate_callbacks_; // Copy
}
for (auto& cb : tmp) { /*...*/ }
Example Usage
CTimedEvent<std::string, int> event;
// Immediate callback
event.subscribe([](std::string msg, int code) {
std::cout << "[IMMEDIATE] " << msg << " (" << code << ")\n";
});
// Delayed callback (2 seconds)
event.subscribe_with_delay([](std::string msg, int code) {
std::cout << "[DELAYED] " << msg << " (" << code << ")\n";
}, 2000);
// Trigger event
event.trigger("Error: DB timeout", 500);
// Output:
// [IMMEDIATE] Error: DB timeout (500)
// [DELAYED] Error: DB timeout (500) [after 2 seconds]
Conclusion
The CTimedEvent
class elegantly
combines:
·
Observer Pattern for decoupled subscriptions.
·
RAII for thread-safe resource management.
·
Asynchronous Threading for delayed tasks.
Use
it for event-driven scenarios requiring flexible timing, but enhance it with
thread pooling and lifetime management for production systems.
Inspiring Innovation:
Design Systems That Fit Your Vision
As you explore the CTimedEvent
class,
remember: great
software design isn’t about rigidly following patterns—it’s about creatively
adapting them to your unique challenges. This
implementation is a starting
point, not a final destination. Ask yourself:
"What if I could..."
·
Scale effortlessly? Replace detached threads with
a dedicated thread
pool to handle thousands of delayed events without
resource exhaustion.
·
Add precision? Integrate a priority
queue for callbacks sorted by execution time, turning CTimedEvent
into a
high-precision scheduler.
·
Enhance resilience? Wrap callbacks in retry logic or
circuit breakers to handle failures in distributed systems.
Your
Call to Action
1.
Experiment Fearlessly:
o Modify launch_delayed()
to support cancellable timers (e.g.,
abort a delayed email if the user edits it within 5 seconds).
o Add recurring events by
rescheduling callbacks after execution.
2.
Observe Real Needs:
o In a game? Extend this
to predictive
event batching (e.g., "trigger all physics callbacks
in the next 16ms frame").
o Building IoT?
Embed energy-aware
delays (e.g., "execute during low-power
cycles").
3.
Break Boundaries:
o "What if my events
span machines?" Replace std::function
with gRPC
stubs for distributed events.
o "What if timing is
critical?" Swap nanosleep()
for hardware
timers or RTOS primitives.
🔥 Remember: The most
elegant systems emerge when you understand
the rules—then innovate beyond them. Your use case is unique; your
solution should be too. Start simple, iterate boldly, and engineer something
that doesn’t just work—it inspires.
Now go build the event system only YOU can envision. 🚀
This article was enhanced with DeepSeek.