Freigeben über


Asynchronous infinite loops instead of timers

Did it occur to you that an infinite loop, with async/await inside it, isn't really an infinite loop? It looks like one (which is usually bad) but because of the asynchrony, we know that it isn't executing the entire method at one time. Part executes now, then sometime later it periodically resumes – that sounds kind of like a timer!

Let’s say I have some UI work I need to do on a periodic basis. Normally I have two options

  1. Spin up a new Task which has a loop which does a bit of work, updates the UI, then sleeps for a period of time.
  2. Create a DispatchTimer, configure it properly and do the work in the Tick event

The first option gives us code with clear intent (“loop and update forever!”), but it introduces multiple delegates, potential multithreading complexities depending on the work being done and we still have to get back onto the UI thread to update the UI.

 void DoWorkPollingTask()
{
    Task.Run(async () =>
    {
        while (true)
        {
            // do the work in the loop
            string newData = DateTime.Now.ToLongTimeString();

            // update the UI on the UI thread
            Dispatcher.Invoke(() => txtTicks.Text = "TASK - " + newData);

            // don't run again for at least 200 milliseconds
            await Task.Delay(200);
        }
    });
}

whereas the second option, while keeping us on a single thread (the UI thread) spreads our intent across multiple methods because the "repeat" behavior is separate from the "work" behavior (i.e. the only way we know this is a periodic activity is that the method is named _timer_Tick) and may make for more difficult maintenance if it gets more complicated.

 DispatcherTimer _timer = new DispatcherTimer();

void DoWorkTimer()
{
    _timer.Interval = TimeSpan.FromMilliseconds(200);
    _timer.Tick += _timer_Tick;
    _timer.IsEnabled = true;
}

void _timer_Tick(object sender, EventArgs e)
{
    // do the work in the loop
    string newData = DateTime.Now.ToLongTimeString();

    // update the UI on the UI thread
    txtTicks.Text = "TIMER - " + newData;
}

There is a third option. Use an asynchronous loop. Even though this looks like an infinite loop, its really not. Because of the await of the delay Task, this method yields control back to the its caller periodically. If this weren’t an async method with a valid asynchronous operation, it would be a synchronous method and would deadlock the UI when we called it.

But since it is not, it works as needed and gives us both a very clear intent of what the code is supposed to do as well as keeps us on the UI thread the whole time (the magic of await!) so we don't have multithreaded issues or multiple delegates/callbacks.

 private async Task DoWorkAsyncInfiniteLoop()
{
    while (true)
    {
        // do the work in the loop
        string newData = DateTime.Now.ToLongTimeString();

        // update the UI
        txtTicks.Text = "ASYNC LOOP - " + newData;

        // don't run again for at least 200 milliseconds
        await Task.Delay(200);
    }
}

The real trick to making this work though, is not in the DoWorkAsyncInfiniteLoop method. Its how we call it. You do not await the call to it.

 private void bttnStart_Click(object sender, RoutedEventArgs e)
{
    // invoke loop method but DO NOT await it
    DoWorkAsyncInfiniteLoop();
}

This will start the async loop but we do not want our code to await here for it to finish (because it won’t). We treat it as “fire and forget.”

This will also cause a green squiggle in Visual Studio because “Hey you forget to await this obviously async method!!!” We can ignore that, or just stash the returned task into a local variable so it doesn’t complain. What I will not do to “fix” the green squiggle is make the looping method return void instead of Task. Always return a Task, you never know who might want it in the future (maybe a future change also introduces a cancellation feature).

Other thoughts:

  • Should I write code this way? As always, it depends. But having one more option doesn't hurt.
  • Of course these don’t have to be infinite loops, we could institute some kind of flag or cancellation mechanism, but this way makes for simpler example code and a more interesting blog title!
  • Don’t do this if you have a large amount of work to do. This is all happening on the UI thread so each time around the loop, its grabbing that UI thread and using it (which means the UI is not responsive at that time). If the work is significant, then Task.Run is a probably better option to offload the work onto another thread.
  • The simple example here doesn't prevent reentrancy. If something calls the method again, you will get a second  loop running in addition to the original loop. It won't be parallel (still only on the UI thread) but will be interleaved with the rest of the UI thread work.

The above sample source is available on my GitHub.

Comments

  • Anonymous
    June 30, 2016
    A couple of comments:1) With "fire and forget" work, any exceptions are ignored. So, for "top-level" loops like this, it's a good policy to always have a try/catch around the entire method.2) Bear in mind that a timer fires on a strictly periodic basis and resets immediately, so if you're doing 50ms of work every 200ms, with a timer your work would be executed at t+0, t+200, t+400, t+600, etc. However, the basic async loop waits between work, so in the same scenario with an async loop, your work would be executed at t+0, t+250, t+500, t+750, etc.
    • Anonymous
      July 01, 2016
      Yes, those are important points on both counts.For the timing, I've noticed an interesting trend in the code I've reviewed from customers. They often will disable the timer when they enter the event handler and re-enable the timer at the end of the delegate. I'm not sure why this is so (its also just my own anecdotal experience) but that does create the "start the next work 200ms after I'm done working now" behavior instead of the "try to start my work every 200ms." Either way it is an important logical difference, especially if your work is a significant fraction of 200ms.thanks!
      • Anonymous
        June 08, 2018
        Very good article! Thank you, BenWilli, for mentioning those subtle differences that remain current as of today. You have been able to express things to the point, in a concise and precise manner! You can make a great lecturer, if you are not one yet! -:)
      • Anonymous
        March 06, 2019
        Sorry, old post I know, just following up on why developers would disable the timer when entering the event handler. I've seen this pattern a quite a bit in the old code I sift through. It's brittle, but it's an attempt to keep the work from queuing up if the work might take longer than the interval. My strong opinion is that previous developers we unaware of better practices and it was good enough at the time.
        • Anonymous
          March 06, 2019
          (edited comment after reviewing the whole thread) Yes, that would be why. Thanks! Sometimes good enough is good enough.
          • Anonymous
            April 24, 2019
            I'm not sure which would be faster, but if the work done in the loop is significantly larger than the work done by the scheduling mechanism (which I would assume) it would be a moot point.
  • Anonymous
    March 06, 2019
    Hello, thank you for this article.I am beginner in asynchronous programming and try to find and understand the correct way for an application.In the first example what is the goal of aync/await inside the task?Task is already asynchronous isnt it?Would not it be redundant to use aync/await inside the task?
    • Anonymous
      March 06, 2019
      In the first example the code inside the Task.Run() is only asynchronous because I chose to use Task.Delay() instead of Thread.Sleep(). I could have used Sleep(200) and the behavior would have been basically the same and then the method would not have needed to be async. Task.Delay() does an asynchronous wait while Thread.Sleep() does a synchronous wait. The Task itself is running asynchronously from the code that created it (on a new thread) but the code it runs inside it can be async or sync just depending on what needs to be done.
  • Anonymous
    April 23, 2019
    So performance wise are Async infinite loops with Task.Delay() better than using the Timers... ?