timerblazorthread-safetyblazor-server-sideperiodictimer

Blazor, PeriodicTimer with Async Methods and IDisposable-- is this correct?


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();
     }
}

Solution

  • 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> (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?

    Consider what you have:

    1. An instance of the PeriodicTimer in memory.
    2. A timer object on the TimerQueue with a reference to a callback method [_state.Signal()] in the PeriodicTimer instance.
    3. A component with a reference to a ValueTaskAwaiter [provided by 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