Use Timers Safely
Schedule native callbacks without mistaking stop or dispose for an in-flight completion barrier.
Two timer families
Sharp Runtime has System::Threading::Timer in the Threading component and System::Timers::Timer in Timers. They are related but have different callback shapes and, crucially, different lifetime risks.
| Type | Model | Current implementation |
|---|---|---|
Threading::Timer | Callback + opaque state | Dedicated thread with shared internal state; safe state lifetime after disposal. |
Timers::Timer | Configurable Elapsed event | Wraps the threading timer but its callback captures the outer this. |
Use the lower-level timer
set(SHARP_RUNTIME_COMPONENTS Threading)
target_link_libraries(app PRIVATE SharpRuntime::Threading)
#include <System/Threading/Timer.hpp>
#include <atomic>
std::atomic<int> ticks{0};
System::Threading::Timer timer(
[&ticks](void*) { ticks.fetch_add(1, std::memory_order_relaxed); },
nullptr,
100, // first callback after 100 ms
250); // then every 250 ms
timer.Change(-1, 250); // pause; -1 means infinite due time
timer.Change(0, 250); // re-arm immediately
timer.Dispose();
Each timer uses a dedicated native thread, not a shared thread pool. Change reschedules the next fire relative to the call. A non-positive period makes the timer single-shot after it fires. The callback must be non-empty; due time and period must be at least -1.
Use the event-style timer carefully
set(SHARP_RUNTIME_COMPONENTS Timers)
target_link_libraries(app PRIVATE SharpRuntime::Timers)
#include <System/Timers/Timer.hpp>
System::Timers::Timer timer(500.0);
timer.setAutoResetProperty(false);
timer.Elapsed += [](System::Object*, const System::Timers::ElapsedEventArgs&) {
// Keep this bounded and report errors inside the handler.
};
timer.Start();
Elapsed runs directly on a background thread. Handler exceptions are caught and discarded, so a failed handler is invisible unless it reports its own status. The current sender is nullptr, not the timer object.
System::Timers::Timer ultimately detaches its background thread and its callback captures this. Stop, Close, Dispose, or destruction does not prove an already-running callback has returned. Coordinate teardown externally and never destroy the timer from its own handler.
Build an external shutdown protocol
- Keep callback data in an owner whose lifetime is longer than the timer.
- Set an application stopping flag before disabling new ticks.
- Count or latch in-flight callbacks in the application state.
- Wait for that count to reach zero from a different thread.
- Only then destroy the event-style timer and callback state.
A callback must not wait for the thread that is waiting for it. Document lock ordering and avoid doing blocking I/O while holding the callback-state mutex.
C# comparison
| C# / .NET | Sharp Runtime | Difference |
|---|---|---|
| Thread-pool timer queue | Dedicated thread per threading timer | Resource cost scales with timer count. |
DisposeAsync/wait-handle disposal | Not reproduced | Application supplies completion coordination. |
Timers.Timer sender is the timer | Sender currently nullptr | Capture explicit stable state instead. |
| Handler exception suppressed by Timers.Timer | Also suppressed | Report failures inside the callback. |
Test time without flaky sleeps
- Use generous upper deadlines and synchronization primitives rather than exact wall-clock equality.
- Test pause, re-arm, single-shot, periodic, and
Changeduring a callback. - Keep test timer counts small; this implementation creates native threads.
- Test teardown under sanitizer builds, especially the event-style outer-object lifetime.
- On single-threaded Emscripten, expect
PlatformNotSupportedException.