【问题标题】:How to cancel await Task.Delay()?如何取消等待 Task.Delay()?
【发布时间】:2014-04-04 20:38:49
【问题描述】:

正如你在这段代码中看到的:

public async void TaskDelayTest()
{
     while (LoopCheck)
     {
          for (int i = 0; i < 100; i++)
          {
               textBox1.Text = i.ToString();
               await Task.Delay(1000);
          }
     }
}

我希望它将文本框设置为 i 的字符串值,持续一秒,直到我将 LoopCheck 值设置为 false 。但它所做的是它为所有创建了所有迭代,即使我将 LoopCheck 值设置为 false,它仍然会异步执行它所做的事情。

当我设置LoopCheck=false 时,我想取消所有等待的Task.Delay() 迭代。如何取消?

【问题讨论】:

  • 我建议查看取消令牌。您不需要循环检查。您需要将令牌传递给该方法并检查它是否在每次延迟后被取消。
  • 完全正确。但正如你可以承认的那样,关于这方面的文档是非常新的,而且还不够。也许这里有人知道如何使用cancellationtoken
  • 其实cancellation documentation已经很详尽了,到现在已经快四年了。

标签: c# async-await


【解决方案1】:

如果您要投票,请在CancellationToken 上投票:

public async Task TaskDelayTestAsync(CancellationToken token)
{
  for (int i = 0; i < 100; i++)
  {
    textBox1.Text = i.ToString();
    await Task.Delay(TimeSpan.FromSeconds(1), token);
  }
}

有关详细信息,请参阅cancellation documentation

【讨论】:

    【解决方案2】:

    使用Task.Delayoverload 接受CancellationToken

    public async Task TaskDelayTest(CancellationToken token)
    {
        while (LoopCheck)
        {
            token.throwIfCancellationRequested();
            for (int i = 0; i < 100; i++)
            {
                textBox1.Text = i.ToString();
                await Task.Delay(1000, token);
            }
        }
    }
    
    var tokenSource = new CancellationTokenSource();
    TaskDelayTest(tokenSource.Token);
    ...
    tokenSource.Cancel();
    

    【讨论】:

    • 它绝对有效。但现在还有另一个问题。当我取消任务时,我得到了一个例外。我不能使用 try - catch 因为如果我这样做它会给出 100 次异常。怎么办?我知道我问得太多了,但你似乎很了解这个话题。
    • 你需要决定当一个任务被取消时会发生什么,例如 Try/Catch 绕过延迟,在 Catch 中你可以放'break'来退出循环或'return'来退出整个方法。
    • 当然,使用取消令牌,但最简单的解决方法是合并您的 while 和 for 循环测试; for (int i = 0; i &lt; 100 &amp;&amp; !token.IsCancellationRequested; i++)
    • @TheodorZoulias 而不是投反对票,您为什么不自己用替代解决方案回答这个问题。它可以让你更详细地解释你的推理,例如“它可能会随机抛出”不太确定您的意思。
    • 如果条件token.IsCancellationRequested恰好为真,则while方法无异常返回。如果在等待Task.Delay 期间发生取消,该方法将抛出。你无法判断哪一个会发生,因此是随机性的。我经常看到这种不一致,我认为这是一个缺陷。
    【解决方案3】:
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApp1
    {
        class Program
        {
            static DateTime start;
            static CancellationTokenSource tokenSource;
            static void Main(string[] args)
            {
                start = DateTime.Now;
                Console.WriteLine(start);
    
    
                TaskDelayTest();
    
                TaskCancel();
    
                Console.ReadKey();
            }
    
            public static async void TaskCancel()
            {
                await Task.Delay(3000);
    
                tokenSource?.Cancel();
    
                DateTime end = DateTime.Now;
                Console.WriteLine(end);
                Console.WriteLine((end - start).TotalMilliseconds);
            }
    
            public static async void TaskDelayTest()
            {
                tokenSource = new CancellationTokenSource();
    
                try
                {
                    await Task.Delay(2000, tokenSource.Token);
                    DateTime end = DateTime.Now;
                    Console.WriteLine(end);
                    Console.WriteLine((end - start).TotalMilliseconds);
                }
                catch (TaskCanceledException ex)
                {
                    Console.WriteLine(ex.Message);
                }
                finally
                {
                    tokenSource.Dispose();
                    tokenSource = null;
                }
            }
        }
    }
    

    【讨论】:

    • 欢迎您。。您的回答可能是对的,但请尝试解释您的答案,而不是简单的代码转储:)
    【解决方案4】:

    遇到这个问题后,我写了一个替代品,如果你想进行轮询,它的行为符合预期:

    public static class TaskDelaySafe
    {
        public static async Task Delay(int millisecondsDelay, CancellationToken cancellationToken)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(millisecondsDelay), cancellationToken);
        }
    
        public static async Task Delay(TimeSpan delay, CancellationToken cancellationToken)
        {
            var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            var task = new TaskCompletionSource<int>();
    
            tokenSource.Token.Register(() => task.SetResult(0));
    
            await Task.WhenAny(
                Task.Delay(delay, CancellationToken.None),
                task.Task);
        }
    }
    

    它使用取消令牌回调来完成任务,然后等待该合成任务或没有取消令牌的正常 Task.Delay。这样当源令牌被取消时它不会抛出异常,但仍然通过返回执行来响应取消。您仍然需要在调用 IsCancellationRequested 后检查它,以决定如果它被取消该怎么办。

    单元测试,如果有人感兴趣的话:

        [Test]
        public async Task TaskDelay_WaitAlongTime()
        {
            var sw = System.Diagnostics.Stopwatch.StartNew();
            await Base.Framework.TaskDelaySafe.Delay(System.TimeSpan.FromSeconds(5), System.Threading.CancellationToken.None);
            Assert.IsTrue(sw.Elapsed > System.TimeSpan.FromSeconds(4));
        }
    
        [Test]
        public async Task TaskDelay_DoesNotWaitAlongTime()
        {
            var tokenSource = new System.Threading.CancellationTokenSource(250);
    
            var sw = System.Diagnostics.Stopwatch.StartNew();
            await Base.Framework.TaskDelaySafe.Delay(System.TimeSpan.FromSeconds(5), tokenSource.Token);
            Assert.IsTrue(sw.Elapsed < System.TimeSpan.FromSeconds(1));
        }
    
        [Test]
        public async Task TaskDelay_PrecancelledToken()
        {
            var tokenSource = new System.Threading.CancellationTokenSource();
            tokenSource.Cancel();
    
            var sw = System.Diagnostics.Stopwatch.StartNew();
            await Base.Framework.TaskDelaySafe.Delay(System.TimeSpan.FromSeconds(5), tokenSource.Token);
            Assert.IsTrue(sw.Elapsed < System.TimeSpan.FromSeconds(1));
        }
    

    【讨论】:

      【解决方案5】:

      关于有一个取消令牌并使用 try-catch 来阻止它抛出异常的一点评论 - 您的迭代块可能由于不同的原因而失败,或者它可能由于不同的任务被取消而失败(例如来自子方法中的 http 请求超时),因此要让取消令牌不引发异常,您可能需要更复杂的 catch 块

      public async void TaskDelayTest(CancellationToken token)
      {
          while (!token.IsCancellationRequested)
          {
              for (int i = 0; i < 100; i++)
              {
                  try
                  {
                      textBox1.Text = i.ToString();
                      await DoSomethingThatMightFail();
                      await Task.Delay(1000, token);
                  }
                  catch (OperationCanceledException) when (token.IsCancellationRequested)
                  {
                      //task is cancelled, return or do something else
                      return;
                  }
                  catch(Exception ex)
                  {
                       //this is an actual error, log/throw/dostuff here
                  }
              }
          }
      }
      

      【讨论】:

      • 另外,您可能想要捕获 OperationCanceledException,因为您将捕获所有类型的已取消异常,而不仅仅是 TaskCanceledException。
      • 很糟糕。 DoSomething... 也可以抛出OperationCanceledException。应该是:catch (TaskCanceledException tce) when (tce.CancellationToken == token)
      • @apocalypse 一些第三方(或第一方)方法可能会抛出 OperationCanceledException,cancellationTokens 可以传递给异步或同步操作 - 因此使用更通用的 OperationCancelled 异常更正确。但是,在 (oce.CancelationToken == token) 时执行 Catch (OperationCanceledException oce) 是一个好主意,无论您使用它还是何时 (token.IsCancellationRequested) 都取决于您的用例。对我来说,如果我在 HTTPTimeout 的同时取消了某些东西(例如),我并不真正关心超时,我只希望我的取消代码运行。
      猜你喜欢
      • 2015-01-04
      • 2015-03-29
      • 1970-01-01
      • 2020-04-06
      • 2013-04-23
      • 1970-01-01
      • 1970-01-01
      • 2015-12-02
      • 2019-10-19
      相关资源
      最近更新 更多