Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replacing Threading.Timer with a custom async timer class?

I'm trying to replace my old Threading.Timer code with something that can handle an async Function. The reason I'm doing this is that you cannot pass a Threading.Timer an async function without making it an Async Sub, which I am to understand is a bad idea.

Code I wish to replace;

Dim SaveTimer as New Threading.Timer(AddressOf SaveToFile, Nothing, 1000 * 60 * 5, 1000 * 60 * 5)

SaveToFile is an async function, so I get an "The Task returned from this Async Function will be dropped, and any exceptions in it ignored. Consider changing it to an Async Sub so its exceptions are propagated" warning.

The purpose of this timer is to save the contents of the class it's in to a file every 5 minutes. However, I also occasionally call SaveToFile manually and if I do I want to reset the timer.

With the Threading.Timer I would do this;

  Public Async Function ManuallySave As Tasks.Task
    SaveTimer.Change(Timeout.Infinite, Timeout.Infinite)
    Await SaveToFile
    SaveTimer.Change(1000 * 60 * 5, 1000 * 60 * 5)
  End Function

Now I know how to schedule something to be done with Tasks using Task.Delay but I'm not sure how to stop it and reset.

Here's my attempt at an async timer class;

Public Class AsyncTimer

  Private CancellationToken As New CancellationTokenSource
  Private DelegateAction As Action(Of Tasks.Task) = Nothing

  Public Sub New(DelegateAction As Action(Of Tasks.Task))
    Me.DelegateAction = DelegateAction
  End Sub

  Public Async Function StartTimer(Interval As Integer, Optional Repeat As Boolean = False) As Tasks.Task
    CancellationToken = New CancellationTokenSource
    Do
      Await Tasks.Task.Delay(Interval, CancellationToken.Token).ContinueWith(DelegateAction, CancellationToken.Token, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Current)
    Loop Until Not Repeat OrElse CancellationToken.IsCancellationRequested
  End Function

  Public Sub StopTimer()
    CancellationToken.Cancel()
  End Sub

End Class

What concerns me is that I call StopTimer, then shortly afterwards StartTimer, which replaces CancellationToken with a new instance. I'm worried that before the cancellation of the token takes effect I'm overwriting it.

What is the solution to this?

I know the code is in VB but I understand C# just as well, so either as an answer would be fine.

EDIT1: This needs to be thread-safe. This is the cause of my concern.

like image 921
iguanaman Avatar asked Sep 13 '26 18:09

iguanaman


1 Answers

Timer ticks are queued to the thread-pool. You can use async and await there without problems. Make your tick function call an async Task function and throw away the resulting task. And document, why.

void TimerProc(...) {
 var task = TimerImpl();
 //throw away task
}

async Task TimerImpl() {
 try { MyCode(); }
 catch (Exception ex) { Log(ex); }
}

Note, that timer ticks can be delayed arbitrarily, they can execute concurrently and they can run after you have stopped the timer.

I don't see why you'd need to use async code anyway. You are already on the thread-pool so you don't have to unblock the UI. Writing to local disks does not benefit much from async IO. Maybe the answer is: Just use synchronous code here.

In order to guard against overwriting the CancellationToken you need to copy it to all places that uses it. Nothing should directly use the field.

Better yet, create a new instance of AsyncTimer instead of making it restartable. Avoid state mutation.

like image 64
usr Avatar answered Sep 15 '26 11:09

usr