Short version: does the following look thread safe to you?
The following pseudocode is simplified from working code. It updates a SQL database entry every 10 seconds, only if a change has been made to the content of an HTML editor (I use TinyMCE).
My question is this-- when the original page's conditional statement "erases" the instance of <MyEditor>
(when SelectedItem is set to null), what happens to that instance? Does it immediately go to Dispose, and make a clean break? Will the PeriodicTimer or async events potentially throw?
MyPage.razor
@if (SelectedItem is not null){
<MyEditor @bind-HTML=SelectedItem.HTML @OnDeselect="()=>SelectedItem = null" />
}
@code {
List<htmlItem> htmlItems = new(); // Assume I get these in init.
htmlItem? SelectedItem;
async Task SomeSelectionMethod(){
SelectedItem = htmlItems.Single(hi=> somecriteria);
}
}
MyEditor.razor
@implements IDisposable
<MyMCE @bind-HTML=ActiveItem HTML @bind-HTML:after=HandleChangedHTML />
<button @onclick=DeselectMe > Return </button>
@code {
[Parameter]
public htmlItem ActiveItem {get; set;}
[Parameter]
public EventCallback OnDeselect {get; set;}
private PeriodicTimer _timer;
private CancellationTokenSource _cts = new();
private bool NeedsSave;
private async Task DeselectMe()
{
await SaveContentAsync();
await OnDeselect.InvokeAsync();
}
protected override void OnInitialized()
{
_timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
_ = SaveContentPeriodically();
}
private async Task SaveContentPeriodically()
{
try
{
while (await _timer.WaitForNextTickAsync(_cts.Token))
{
if (NeedsSave)
{
await SaveContentAsync();
NeedsSave = false;
}
}
}
catch (OperationCanceledException)
{
// Handle the cancellation
}
}
private async Task SaveContentAsync()
{
await TS.UpdateHtmlItemAsync(ActiveItem); // worried about this. . .
}
async Task HandleChangedHTML()
{
NeedsSave = true;
}
public void Dispose()
{
_cts.Cancel();
_timer.Dispose();
}
}
A little pertinent background information first.
Behind the scenes there's only one timer. TimerQueue
implements the Singleton pattern : one instance per AppDomain. It manages all the application timers and schedules the callbacks when timers expire. TimerQueue
runs on it's own thread. Creating and destroying timers is not expensive. Most registered timers [at any point in time] will be operational timeouts: created and destroyed frequently and only firing if something goes wrong.
Basically, whatever abstraction class you use, it queues a timer object on TimerQueue
with a callback to invoke when the timer expires.
My question is this-- when the original page's conditional statement "erases" the instance of
<MyEditor>
(whenSelectedItem
is set to null), what happens to that instance? Does it immediately go toDispose
, and make a clean break? Will thePeriodicTimer
or async events potentially throw?
Consider what you have:
PeriodicTimer
in memory._state.Signal()
] in the PeriodicTimer
instance.WaitForNextTickAsync
] in the PeriodicTimer
instance.When MyEditor
is removed from the RenderTree by the renderer, it runs Dispose
on the component, and releases it's reference to it. The component's while
loop will still hold a reference to the ValueTaskAwaiter
until it completes and the loop exits.
Calling Dispose
on PeriodicTimer
does the following:
public void Dispose()
{
GC.SuppressFinalize(this);
_timer.Dispose();
_state.Signal(stopping: true);
}
_timer
is a TimerQueueTimer
. Disposing it removes it from the TimerQueue - no more callbacks to _state.Signal()
.
_state
is an internal IValueTaskSource
class that provides programmatic control over a ValueTask
. _state.Signal(stopping: true)
tells Signal
to complete the ValueTaskAwaiter immediately (and return true
). Any subsequent requests to WaitForNextTickAsync
will return false
immediately.
When the ValueTask
completes, the while loop in the component loops. The next
await _timer.WaitForNextTickAsync(_cts.Token)
returns false
, the loop exits, and all ValueTaskAwaiter references are released.
We now have a component instance in memory with no external references and a PeriodicTimer
with no external references.
Everything completes gracefully and the GC will destroy the objects.
You can see the source code for PeriodicTimer
here - https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs,d44c3c480b4836ad