Adding a short delay within the loop prevents loop continuity. What for?

When using the async / await.NET API, I ran into a curiosity: the loop ignored the delay used as a timeout until I added a short delay inside the loop. How it works? Not the most intuitive behavior!

Complete 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 is done

never installed in true

. The loop causes a lot of calls await

. The call Task.WhenAny

is in the unit test to prevent hangs Loop()

. If I present an error in most cases, the test does indeed time out, but sometimes it still just hangs.

The proposed workaround that does not require Task.Delay

inLoop()

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

      

This will start a new thread running the method Loop()

.

Related questions

When will I use Task.Yield ()?

+3


source to share


2 answers


when you wait for a "Task" which first checks to see if the task has completed, if it does, it just continues execution and never returns to the caller. Because of this, the call await DoWork();

will never force you back to the calling method, it will just continue synchronously in the method.

When you remove the delay, you now have the equivalent

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

      



so the loop will loop forever without returning control to the caller. In situations like this, when you don't know if you will fall back to the caller or not, and you want to ensure that you don't loop forever, you can 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.
    }
}

      

+5


source


The current task Loop()

will be executed forever with the condition while(true)

:

public static async Task Loop()
{
    while(true) { } // this iteration will never end.
                    // as noted by Scott Chamberlain answer, the caller will
                    // never regain control of this task
}

      

You should consider passing in CancellationToken

to break your cycle.

public static async Task Loop(CancellationTokenSource cts)
{
    while (cts != null && !cts.IsCancellationRequested)
    {
        // your work you want to keep iterating until cancelled
    }
}

      




I borrowed from this answer to help explain, which is also consistent with my suggestion:

When the first task completes, consider whether to cancel the remaining tasks. If other tasks are not canceled but also never waited, then they are abandoned. Abandoned tasks will run to completion and their results will be ignored. Any exceptions to those abandoned tasks will also be ignored.

Additional Resources: Create .Timeout Task After Method Extension

+3


source







All Articles