Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding a short delay inside a loop prevents it from looping indefinitely. Why?

While using the .NET async/await API I ran into a curiosity: a loop was ignoring a delay used as a timeout, until I added a short delay inside the loop. How does this work? Not the most intuitive behavior!

Full program:

using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main(String[] args)
    {
        Task.Run(async () =>
        {
            await Task.WhenAny(Loop(), Task.Delay(TimeSpan.FromSeconds(1)));
            Console.WriteLine("Timed out!");
        })
        .Wait();
    }

    public static async Task Loop()
    {
        while(true)
        {
            // Commenting this out makes the code loop indefinitely!
            await Task.Delay(TimeSpan.FromMilliseconds(1));

            // This doesn't matter.
            await DoWork();
        }
    }

    public static async Task DoWork()
    {
        await Task.CompletedTask;
    }
}

Background

The actual program has while(!done) but due to a bug done is never set to true. The loop makes many await calls. The Task.WhenAny call is in a unit test to prevent Loop() from hanging. If I introduce a bug on purpose most of the time the test indeed times out, but sometimes it still just hangs.

Suggested workaround that doesn't require Task.Delay in Loop()

bool completedOnTime = Task.Run(() => Loop()).Wait(TimeSpan.FromSeconds(1));

This will start a new thread executing the Loop() method.

Related questions

When would I use Task.Yield()?

like image 785
Konrad Jamrozik Avatar asked Dec 23 '22 17:12

Konrad Jamrozik


1 Answers

when you await a Task it first checks to see if the task is complete, if it is complete it just continues the execution and never returns to the caller. Because of this the call to await DoWork(); will never cause you to return to the calling method, it will just synchronously continue on in the method.

When you remove the delay you now have the equivalent of having

public static async Task Loop()
{
    while(true)
    {
    }
}

so the loop will loop forever without ever giving control back to the caller. In situations like this where you don't know if you will be returning to the caller or not and you want to guarantee you don't loop forever you could rewrite your code as

public static async Task Loop()
{
    while(true)
    {
        var workTask = DoWork();
        if(workTask.GetAwaiter().IsCompleted) //This IsCompleted property is the thing that determines if the code will be synchronous.
            await Task.Yield(); //If we where syncronous force a return here via the yield.
        await workTask; //We still await the task here in case where where not complete, also to observe any exceptions.
    }
}
like image 170
Scott Chamberlain Avatar answered Dec 29 '22 05:12

Scott Chamberlain