如何取消 await Task.Delay()?

41

如您所见,这段代码:

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()迭代。如何取消它?


1
我建议您研究一下取消令牌。您不需要循环检查。您需要将一个令牌传递到方法中,并在每个延迟后检查它是否已被取消。 - Smeegs
没错。但是正如你所知道的,关于这方面的文档非常新而且不足。也许在这里有人知道如何使用cancellationtoken。 - Zgrkpnr
3
实际上,取消文档非常详尽,并且已经存在了将近四年的时间。 - Stephen Cleary
避免使用异步 void。 - Theodor Zoulias
5个回答

58

使用接受 CancellationTokenTask.Delay重载方法

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();

6
完全起作用了。但现在出现了另一个问题,当我取消任务时,会收到一个异常。我无法使用try-catch,因为这样做会导致异常发生100次。现在怎么办?我知道我要求太多了,但你似乎非常了解这个话题。 - Zgrkpnr
9
你需要决定任务取消时会发生什么,例如在延迟周围使用Try/Catch,在Catch中,你可以放置“break”以仅退出循环或“return”以退出整个方法。 - WhoIsRich
3
可以使用取消令牌,但最简单的解决方法是合并 while 和 for 循环的测试条件;for (int i = 0; i < 100 && !token.IsCancellationRequested; i++) - Jeremy Lakeman
5
与其给出负面评价,为什么不用一种替代方案自己回答问题呢?这将使你能够更详细地解释你的推理,例如“它可能会随机抛出”,我不太确定你的意思是什么。 - James
3
如果条件 token.IsCancellationRequested 为真,则方法将返回而不抛出异常。如果在等待 Task.Delay 期间发生取消操作,则该方法将引发异常。无法确定哪种情况会发生,因此具有随机性。我经常看到这种不一致性,并认为这是一个缺陷。 - Theodor Zoulias
显示剩余9条评论

11

如果你要进行轮询,就在 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);
  }
}

欲了解更多信息,请参阅取消文档


9

关于使用取消令牌和使用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
            }
        }
    }
}

2
另外,您可能希望捕获OperationCanceledException,因为这样可以捕获所有类型的取消异常,而不仅仅是TaskCanceledExceptions。 - Daniel James Bryars
很糟糕。做某事...也可能会抛出“OperationCanceledException”。应该是:catch (TaskCanceledException tce) when (tce.CancellationToken == token) - apocalypse
@apocalypse 一些第三方(或第一方)方法可能会抛出OperationCanceledException,可以将cancellationTokens传递给异步或同步操作-因此使用更通用的OperationCancelled异常更正确。不过,当(oce.CancelationToken == token)时捕获OperationCanceledException oce是个好主意,无论你使用它还是(token.IsCancellationRequested),这取决于你的用例。对我来说,如果我在HTTPTimeout(例如)同时取消了某些操作,我并不真正关心超时,我只想让我的取消代码运行。 - The Lemon

0

在遇到这个问题后,我编写了一个替代品,如果您想进行轮询,则会按预期运行:

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

-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
嘿,欢迎你。你的答案可能是对的,但请尽量解释你的回答,而不是简单地放上代码 :) - Ali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接