Run async method regularly with specified interval

53,113

Solution 1

The async equivalent is a while loop with Task.Delay (which internally uses a System.Threading.Timer):

public async Task PeriodicFooAsync(TimeSpan interval, CancellationToken cancellationToken)
{
    while (true)
    {
        await FooAsync();
        await Task.Delay(interval, cancellationToken)
    }
}

It's important to pass a CancellationToken so you can stop that operation when you want (e.g. when you shut down your application).

Now, while this is relevant for .Net in general, in ASP.Net it's dangerous to do any kind of fire and forget. There are several solution for this (like HangFire), some are documented in Fire and Forget on ASP.NET by Stephen Cleary others in How to run Background Tasks in ASP.NET by Scott Hanselman

Solution 2

The simple way of doing this is using Tasks and a simple loop:

public async Task StartTimer(CancellationToken cancellationToken)
{

   await Task.Run(async () =>
   {
      while (true)
      {
          DoSomething();
          await Task.Delay(10000, cancellationToken);
          if (cancellationToken.IsCancellationRequested)
              break;
      }
   });

}

When you want to stop the thread just abort the token:

cancellationToken.Cancel();

Solution 3

Here is a method that invokes an asynchronous method in periodic fashion:

public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
    CancellationToken cancellationToken = default)
{
    while (true)
    {
        var delayTask = Task.Delay(interval, cancellationToken);
        await action();
        await delayTask;
    }
}

The supplied action is invoked every interval, and then the created Task is awaited. The duration of the awaiting does not affect the interval, unless it happens to be longer than that. In that case the principle of no-overlapping-execution takes precedence, and so the period will be extended to match the duration of the awaiting.

In case of exception the PeriodicAsync task will complete with failure, so if you want it to be error-resilient you should include rigorous error handling inside the action.

Usage example:

Task statisticsUploader = PeriodicAsync(async () =>
{
    try
    {
        await UploadStatisticsAsync();
    }
    catch (Exception ex)
    {
        // Log the exception
    }
}, TimeSpan.FromMinutes(5));

.NET 6 update: It is now possible to implement an almost identical functionality without incurring the cost of a Task.Delay allocation on each loop, by using the new PeriodicTimer class:

public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
    CancellationToken cancellationToken = default)
{
    using var timer = new PeriodicTimer(interval);
    while (true)
    {
        await action();
        await timer.WaitForNextTickAsync(cancellationToken);
    }
}

The WaitForNextTickAsync method returns a ValueTask<bool>, which is what makes this implementation more efficient. The difference in efficiency is pretty minuscule though. For a periodic action that runs every 5 minutes, allocating a few lightweight objects on each iteration should have practically zero impact.

The behavior of the PeriodicTimer-based implementation is not identical with the Task.Delay-based implementation. In case the duration of an action is longer than interval, both implementations will invoke the next action immediately after the completion of the previous action, but the scheduler of the PeriodicTimer-based implementation will not slide forward like the Task.Delay-based implementation does. See the marble diagram below for a visual demonstration of the difference:

Clock          X---------X---------X---------X---------X---------X---------X--
Task.Delay:    +-----|   +---|     +------------|+---|     +------|  +--|
PeriodicTimer: +-----|   +---|     +------------|+---| +------|  +--|      +--

The scheduling of the Task.Delay-based implementation was permanently shifted forward, because the third invocation of the action lasted longer than the interval.

Share:
53,113

Related videos on Youtube

Eadel
Author by

Eadel

Updated on February 13, 2022

Comments

  • Eadel
    Eadel about 2 years

    I need to publish some data to the service from the C# web application. The data itself is collected when user uses the application (a kind of usage statistics). I don't want to send data to the service during each user's request, I would rather collect the data in the app and send then all the data in a single request in a separate thread, that does not serve the users requests (I mean user does not have to wait for the request to be processed by the service). For this purpose I need a kind of JS's setInterval analog - the launch of the function each X seconds to flush all collected data to the service.

    I found out that Timer class provides somewhat similar (Elapsed event). However, this allows to run the method only once, but that's not a big issue. The main difficulty with it is that it requires the signature

    void MethodName(object e, ElapsedEventArgs args)
    

    while I would like to launch the async method, that will call the web-service (input parameters are not important):

    async Task MethodName(object e, ElapsedEventArgs args)
    

    Could anyone advise how to solve the described task? Any tips appreciated.

    • Alexei Levenkov
      Alexei Levenkov almost 9 years
      If Timer does not want to cooperate with you to "AutoReset - Gets or sets a value indicating whether the Timer should raise the Elapsed event each time the specified interval elapses or only after the first time it elapses." because you refused to read link you've provided than you are out of luck... :) Side note - generally it is better to use external notifications if you need reliable intervals/avoid recycle of process - "bing.com/search?q=asp.net+repeating+task" should give you some info (or "ASP.Net background tasks").
    • Eadel
      Eadel almost 9 years
      @AlexeiLevenkov nice taunt. Thanks for the tips, will explore more carefully next time. However, it does not solve the main issue.
    • Alexei Levenkov
      Alexei Levenkov almost 9 years
      Since it does not address main issue it is a comment... Answer by i3arnon gives you links how to call async in fire and forget manner (also known as "call async method from event handler")...
  • i3arnon
    i3arnon almost 9 years
    1. No need for Task.Run. 2. DoSomething should be async. 3. You should await the task returned form Task.Delay
  • Saber
    Saber over 7 years
    Should I write "await PeriodicFooAsync" or just "var result = PeriodicFooAsync"? Do we need await in this case?
  • i3arnon
    i3arnon over 7 years
    @Arvand it depends if you want to wait for the operation to complete or not. result in your case will only hold the task, the next line after that will happen immediately. await will "block" asynchronously until the periodic task will be cancelled.
  • i3arnon
    i3arnon over 7 years
    @Arvand you probably want to save that task somewhere without awaiting it (assuming it lives forever until your application shuts down) and then you can await it before closing.
  • Saber
    Saber over 7 years
    Since I have multiple PeriodicAsycMethods I should certainly save the task and avoid using await, Thank you.
  • Saber
    Saber over 7 years
    Took me a while to understand how to write it in a general way to accept any task and run it periodiclly, I actually got help from your other answer stackoverflow.com/questions/27918061/…. Maybe you can update your answer so others won't go through the same problem. @i3arnon
  • pitersmx
    pitersmx almost 7 years
    This code throws System.Threading.Tasks.TaskCanceledException on await Task.Delay(...) line.
  • tymtam
    tymtam over 6 years
    That's not periodic as await FooAsync will have a non zero execution time. For example, if FooAsync takes 500ms and runs every hour than you will be off by 10 minutes in less than 2 months.
  • tymtam
    tymtam over 6 years
    That's not periodic as DoSomething will have a non zero execution time. For example, if DoSomething takes 500ms and runs every hour than you will be off by 10 minutes in less than 2 months.
  • i3arnon
    i3arnon over 6 years
    @mayu that's the point. Imagine you run it every 5 minutes and it takes 7. It doesn't need to align with a clock (it doesn't start aligned to begin with). It needs to happen every x interval without running over itself.
  • tymtam
    tymtam over 6 years
    @i3arnon Of course it solves the OP's problem. I'm just pointing out that it is not very regular. It doesn't run "every n hours/minutes/seconds".
  • i3arnon
    i3arnon over 6 years
    @mayu I understand. I'm saying It shouldn't. If you run at a tight interval (which you probably can't guarantee accurately anyway) you risk your action running over the interval and introducing unintentional cascading concurrency. That may cause operations to run even slower which causes more concurrency, etc. That's a bad practice.
  • Claudio Ferraro
    Claudio Ferraro almost 5 years
    Even though the use of "async" and "await" is a "trendy" and "cool" approach, You have to keep in mind that async doesn't run Your task in a separate thread, it just defers the execution like javascript does. Please consider to use Threads if You'll run in situations where performance is important and You face a limit situation of many resources consumed by the system.
  • Theodor Zoulias
    Theodor Zoulias almost 4 years
    Suggestion for improvement: You could have a more consistent interval by creating the Task.Delay task before awaiting the FooAsync and awaiting it afterwards. This will make a difference if the FooAsync does some non-trivial work synchronously before returning a Task.
  • i3arnon
    i3arnon almost 4 years
    @TheodorZoulias That's intentional. Imagine what happens if FooAsync takes longer than the delay interval for some reason..
  • Theodor Zoulias
    Theodor Zoulias almost 4 years
    If the awaiting of FooAsync takes longer than the interval, then with my suggestion the period will be extended and essentially defined by the duration of FooAsync. In that case a more precise name for the interval argument would be minInterval. Btw the current version of PeriodicFooAsync is not periodic. It's not trying to sustain a constant period between each invocation of the FooAsync. Instead it just imposes a delay between finishing one operation and starting the next.
  • i3arnon
    i3arnon almost 4 years
    @TheodorZoulias again, that's intentional. Your suggestion (in that case) will cause a constant tight (while (true)) loop, which is almost always a bad idea. If you require a exact cadence the solution should be much more robust.
  • sommmen
    sommmen almost 4 years
    hmm. would this be more or less performant than using a timer do you recon?
  • Theodor Zoulias
    Theodor Zoulias almost 4 years
    @sommmen I believe that performance-wise should be equal, because both System.Timers.Timer and Task.Delay are using a System.Threading.Timer internally.
  • kuklei
    kuklei over 3 years
    @tymtam if DoSomehthing is async and we DO NOT await for it then it will not add anything to our periodic execution. The only problem would be if DoSomethings takes longer than our periodic check which should be solved with an object lock on DoSomething to avoid re-entry
  • kuklei
    kuklei over 3 years
    About the task TaskCanceledException that @pitersmx mentions, this is the default behaviour of the task library and something every task should implement as per this source. You could either wrap the Task.Delay in a try/catch block or simply ignore the cancellation token on that task