Use Timers Safely

Schedule native callbacks without mistaking stop or dispose for an in-flight completion barrier.

AdvancedThreading + TimersSource-checked

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.

TypeModelCurrent implementation
Threading::TimerCallback + opaque stateDedicated thread with shared internal state; safe state lifetime after disposal.
Timers::TimerConfigurable Elapsed eventWraps 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.

Stopping is not a callback-completion barrier

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

  1. Keep callback data in an owner whose lifetime is longer than the timer.
  2. Set an application stopping flag before disabling new ticks.
  3. Count or latch in-flight callbacks in the application state.
  4. Wait for that count to reach zero from a different thread.
  5. 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# / .NETSharp RuntimeDifference
Thread-pool timer queueDedicated thread per threading timerResource cost scales with timer count.
DisposeAsync/wait-handle disposalNot reproducedApplication supplies completion coordination.
Timers.Timer sender is the timerSender currently nullptrCapture explicit stable state instead.
Handler exception suppressed by Timers.TimerAlso suppressedReport 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 Change during 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.