Efficient UI Event and Thread Manager for .NET / Unity
Runtime/Threading/Sentinel.cs
HeaderDoc
(c) 2025 Sator Imaging, Licensed under the MIT License https://github.com/sator-imaging/Unity-Fundamentals
With design eliminating unnecessary lock operations, Sentinel provides extreme fast and
efficient way to manage exclusive and/or concurrent tasks and threads.
Basic Usage
readonly SentinelToken _token = Sentinel.GetUniqueToken();
// use `ExclusiveScope()` method to define exclusive operation block
using (Sentinel.ExclusiveScope(_token, out var rejected)
{
if (rejected) return;
// do the exclusive task
}
// use `DebounceScope()` with blockless-using pattern
using var _ = Sentinel.DebounceScope(_token, 3, out var rejected);
if (rejected)
return; // only first 3 thread or event can enter
Async Callback Handling
Even if callback is invoked only on main thread, event handler may run multiple times if it is marked async.
(ex. multiple button clicks may invoke multiple callbacks)
Here shows how to prevent multiple event invocations in single thread app.
// technically, async method is immediately finished so there is chance to run multiple times
myEvent.Subscribe(async () =>
{
using (Sentinel.SingleThreadScope(_token, out var entrantCount)
{
// check current entrant count
if (entrantCount != 0)
return;
await FooAsync();
await Task.Delay(1000);
}
});
Advanced Usage
Sentinel providing features to eliminate insane Rx operator chains.
UI Event Handling
Make ThrottleFirst or Debounce in modern C# style.
myButton.onClick += async () =>
{
// use shared token to allow only a button can work at a moment
using (Sentinel.ExclusiveScope(_sharedTokenAcrossButtons, out var rejected)
{
// other buttons won't work until this event has finished
if (rejected) return;
myButton.enable = false;
try
{
OnMyButtonClick();
// only allow click once in second, to achieve balancing event stream
await Task.Delay(1000);
}
finally
{
myButton.enable = true;
}
// reaches here after Task.Delay operation is finished.
// and automatically free up exclusive lock by IDisposable.
}
};
Retry Operation
You can write better UX code more simple way rather than using Rx operator chaining.
// case 1) retry entering exclusive scope
RETRY_ENTER:
using (Sentinel.ExclusiveScope(_token, out var rejected)
{
if (rejected)
{
await Task.Delay(100);
goto RETRY_ENTER;
}
// case 2) retry network access
int retryCount = 0;
int delay = 1000;
while (true)
{
if (ct.IsCancellationRequested)
break;
try
{
if (retryCount > 0)
{
await Task.Delay(delay, ct).ConfigureAwait(false);
// can easily implement exponential backoff
delay *= 2;
}
await myNetworkClient.GetAsync(something, timeout: 3000, ct);
break;
}
catch (TimeoutException)
{
retryCount++;
if (retryCount > 10)
throw;
// achieve better app UX without insane Rx techniques
if (retryCount > 3)
ShowToastNotification("Server now gets many traffic. Thank you for your patience.");
}
catch (OperationCanceledException)
{
break;
}
}
}
Extendable Delay Operation
Here shows how to achieve "extendable wait" in UI events or multi-threaded functions. (ex. invoke callback after a second since slider dragging is finished)
private int m_waitDuration;
private int m_latestValue;
// event/thread always updates wait duration even if cannot enter exclusive block to
// prevent event invocation. as a result, event listener invokes only once when a second
// elapsed since last event arrival.
void OnChanged(int value)
{
// always set!!
m_waitDuration = 1000;
m_latestValue = value;
using (Sentinel.ExclusiveScope(_token, out var rejected)
{
if (rejected) return;
// check frequency in milliseconds
const int freq = 100;
// this delay continues until event stream stops.
while ((m_waitDuration -= freq) > 0)
{
await Task.Delay(freq, ct).ConfigureAwait(false);
}
// reaches here a second later since last event.
DelayedAction(m_latestValue);
}
}
Sentinel provides helper method to achieve more accurate delay.
// use timestamp instead of wait duration
private long m_startTimestamp;
// in event listener, repeat delay until time has elapsed
int remaining;
while ((remaining = Sentinel.GetRemainingMilliseconds(m_startTimestamp, 1000)) > 0)
{
await Task.Delay(remaining, ct).ConfigureAwait(false);
}
Technical Notes
If you have encountered error related on SentinelToken, define preprocessor directive
#define STMG_SENTINEL_ENABLE_STRICT_TYPEDEF can solve the problem.