【问题标题】:Adding a short delay inside a loop prevents it from looping indefinitely. Why?在循环中添加一个短暂的延迟可以防止它无限循环。为什么?
【发布时间】:2017-10-20 06:43:58
【问题描述】:

在使用 .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()?

【问题讨论】:

  • 看起来有些不对劲,您是否考虑过在超时任务完成时使用CancellationToken 来停止您的Task Loop()?这里的代码闻起来很有趣,因为您的 Task.Loop() 永远不会停止,而且我看不到有明确的方法来阻止它。
  • 你是对的,实际上需要WhenAny 中的取消令牌来停止它。但是,问题是,我们永远没有机会发出取消请求。这是因为 awaitWhenAny 永远不会返回。
  • @KonradJamrozik 实际上问题更严重,永远不会返回的是Loop,而不是WaitAny,如果您将该调用移至var loopTask = Loop(); await Task.WhenAny(loopTask, Task.Delay(TimeSpan.FromSeconds(1)));,您可以在调试器中看到它.

标签: c# asynchronous async-await task infinite-loop


【解决方案1】:

您当前的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
    }
}

我借用this answer来帮忙解释,这也同意我的建议:

当第一个任务完成后,考虑是否取消 剩余的任务。如果其他任务没有取消但也 从未等待,然后他们被遗弃。 放弃的任务将运行到 完成,其结果将被忽略。 那些被放弃的任务也将被忽略。

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

【讨论】:

  • @Svek 我认为取消可能有副作用(控制台或文件写入,或其他一些可见效果)的任务不是一个好主意。
  • 假设循环条件实际上是“while(!done)”,但由于错误,“done”从未设置为 true。 WhenAny 超时用于测试中,以防止测试在出现错误时挂起。在这种情况下,添加取消令牌检查将相当于用测试代码污染生产代码:(
  • @Svek 但添加取消令牌源只是为了防止出现错误。不是一个干净的设计。我不会反对断言,但我发现很难证明和解释 cts 只是“以防万一”,而不是所需业务逻辑的一部分。
  • @KonradJamrozik 如果您的意图不是停止迭代,请帮助我了解您要完成什么?您可以使用Thread.Sleep() 模拟“长时间运行的过程”,但您的问题表明该方法称为Loop 而不是SampleLongProcess,我希望我说得通。
  • @KonradJamrozik - 我明白你关于使用cts 的观点,因为它可以被理解为使用中断迭代的错误方法。注意到了...您可以轻松地输入自己的论点,例如done,而不是传入cts。对于一个模糊不清的案例,想出一个完整的答案有点困难。
【解决方案2】:

当你等待一个任务时,它首先检查任务是否完成,如果它完成了它只会继续执行并且永远不会返回给调用者。因此,对await DoWork(); 的调用将永远不会导致您返回调用方法,它只会在方法中同步继续。

当您消除延迟时,您现在拥有相当于拥有

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

所以循环将永远循环,而不会将控制权交还给调用者。在这种情况下,你不知道你是否会返回给调用者,并且你想保证你不会永远循环,你可以将你的代码重写为

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

【讨论】:

    猜你喜欢
    • 2016-08-18
    • 2013-10-11
    • 1970-01-01
    • 1970-01-01
    • 2012-11-06
    • 1970-01-01
    • 1970-01-01
    • 2012-05-08
    • 2018-09-07
    相关资源
    最近更新 更多