在循环中添加一个短暂的延迟可以防止它无限循环。为什么?

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

在使用 .NET async/await API 时,我 运行 产生了好奇:一个循环忽略了用作超时的延迟,直到我在循环中添加了一个短暂的延迟.这是如何运作的?不是最直观的行为!

完整节目:

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;
    }
}

背景

实际程序有 while(!done) 但由于错误 done 从未设置为 true。该循环进行多次 await 调用。 Task.WhenAny 调用正在进行单元测试,以防止 Loop() 挂起。如果我故意引入错误,大多数时候测试确实会超时,但有时它仍然会挂起。

建议的解决方法不需要 Loop()

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

这将start a new thread 执行Loop() 方法。

相关问题

When would I use Task.Yield()?

您当前的 Loop() 任务将根据您的 while(true) 条件永远循环:

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

您应该考虑传入一个 CancellationToken 来打破循环。

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

借用帮忙解释一下,也同意我的建议:

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

其他资源:Crafting a Task.TimeoutAfter Extension Method

当您等待任务时,它首先检查任务是否完成,如果完成,它就继续执行,而不会 return 发送给调用者。因此,对 await DoWork(); 的调用永远不会导致您 return 调用方法,它只会在方法中同步继续。

移除延迟后,您现在拥有相当于

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

所以循环将永远循环,而不会将控制权交还给调用者。在这种情况下,您不知道自己是否会 return 调用者,并且您希望保证不会永远循环,您可以将代码重写为

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.
    }
}