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.