How to cancel await Task.Delay()?
Asked Answered
S

8

43

As you can see in this code:

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

I want it to set textbox to string value of i with one second period until I set LoopCheck value to false . But what it does is that it creates all iteration ones in for all and even if I set LoopCheck value to false it still does what it does asyncronously.

I want to cancel all awaited Task.Delay() iteration when I set LoopCheck=false. How can I cancel it?

Starryeyed answered 4/4, 2014 at 20:38 Comment(4)
I would recommend looking into cancellation tokens. You don't need the loop check. You need to pass a token into the method and check if it's been cancelled after each delay.Tiddly
Exactely. But as you can admit, documantation about this is very new and not enough. Maybe someone here knows how to use cancellationtokenStarryeyed
Actually, the cancellation documentation is quite exhaustive and has been up almost four years now.Vauban
Avoid async void.Abfarad
S
62

Use the overload of Task.Delay which accepts a 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();
Solubility answered 4/4, 2014 at 20:51 Comment(14)
It absolutely worked. But now there is another problem. When I cancelled the task I get an exception. I can't use try - catch because if I do that It gives exception 100 times. Now what? I know I am asking too much but you seem to know this topic very well.Starryeyed
You need to decide what happens when a task is cancelled, for example Try/Catch goes around the delay, in the Catch you could put 'break' to just exit the loop or 'return' to exit the whole method.Neurasthenic
Sure, use a cancellation token, but the easiest fix is to merge your while and for loop tests; for (int i = 0; i < 100 && !token.IsCancellationRequested; i++)Eminence
Downvoted because the TaskDelayTest method has an inconsistent cancellation behavior. It might randomly throw or not. I suggest replacing the while (!token.IsCancellationRequested) with while (true) and token.ThrowIfCancellationRequested().Abfarad
@TheodorZoulias rather than downvoting, why don't you just answer the question yourself with an alternative solution. It would allow you to explain in more detail your reasoning e.g. "it might randomly throw" not really sure what you mean by this.Solubility
If the condition token.IsCancellationRequested happens to be true, then the method while return without exception. If the cancellation occurs during the awaiting of Task.Delay, the method will throw. You can't tell which one will happen, hence the randomness. I see this inconsistency quite often, and I consider it a flaw.Abfarad
@TheodorZoulias seems fairly predictable to me - if you cancel during await Task.delay it'll throw (OperationCancelledException?) otherwise it'll just break from the loop and exit. So you can either catch the exception internally and break out or, like you say, throw in both cases. Doesn't seem like a flaw to me, sounds like preference in how you want to manage that scenario...Solubility
I am talking from the perspective of the caller of the TaskDelayTest method. The caller has no way of knowing or predicting or controlling whether a cancellation will result to an exception or not. Actually I just realized that the TaskDelayTest method is async void, which means that the caller will not get any feedback after calling this method. In case of cancellation the caller will not get an exception. The exception will be unhandled, and will crash the process. Which only justifies my downvote even more. 😃Abfarad
TheodorZoulias the fact the exception can't be caught (since it's void) is a good spot, thanks. I can update at some point, or feel free to edit 🙂Solubility
Nahh, I will prefer to limit my involvement at reversing my vote, after the problems have been fixed. 😃Abfarad
@TheodorZoulias not really sure what that means since it's a community-driven site (that's why the option to edit posts exists), nevermind though Ive updated.Solubility
James if you want to incentivise people to improve your answers, you could mark your posts as Community Wikis. This feature prevents the work of one member to unfairly increase the reputation of another member.Abfarad
@TheodorZoulias I have no interest in taking credit for anyone elses work 🙄 the simple fact is I've not worked on the .NET stack probably since this answer was written (it's a 6 year old answer...) so merely suggesting since it's a fresh problem for you feel free to improve the answer. If you recall, I suggested you post your own answer too...Anyway, I appreciate the critique and hopefully the update will help future readersSolubility
There was no point at posting my own answer since this question had already an excellent answer (Stephen Cleary's answer). And now yours is excellent too. Thanks for receiving well my somewhat harsh critique. Opening my first comment with the word "downvoted" was certainly not the kindest from my part 😃.Abfarad
V
12

If you're going to poll, poll on a 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);
  }
}

For more information, see the cancellation documentation.

Vauban answered 4/4, 2014 at 20:52 Comment(0)
P
11

Just a slight comment about having a cancellation token, and using a try-catch to stop it throwing an exception - your iteration block might fail due to a different reason, or it might fail due to a different task getting cancelled (e.g. from an http request timing out in a sub method), so to have the cancellation token not throw an exception you might want a bit more complicated catch block

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
            }
        }
    }
}
Powder answered 16/3, 2020 at 1:42 Comment(3)
Also you might want to catch OperationCanceledException because you will catch all types of Canceled Exceptions, not just the TaskCanceledExceptions.Modestomodesty
It is bad. DoSomething... can throw OperationCanceledException too. It should be: catch (TaskCanceledException tce) when (tce.CancellationToken == token)Gurkha
@Gurkha Some third party (or first party) methods might throw an OperationCanceledException, cancellationTokens can be passed to async or sync operations - so using the more general OperationCancelled exception is more correct. Doing Catch (OperationCanceledException oce) when (oce.CancelationToken == token) is a good idea though, whether you use that or when (token.IsCancellationRequested) just depends on your use case. For me, if I've cancelled something at the same time as an HTTPTimeout (for example), I don't really care about the timeout, I just want my cancel code to run.Powder
V
1

I've been also looking to solve this issue, surprisingly haven't found any correct solution. The problem is that when you trigger 'cancel' the state remains and even if you catch the exception the problem is not solved since the loop delay ignored on next iterations and code becomes very inefficient. So the full solution must be:

public class InfiniteCancelableTaskDelay{

private CancellationTokenSource _cancelableSource = new CancellationTokenSource();

private async void AsyncTask()
{
    while (true)
    {
        //DO SOMETHING 
        try
        {
            await Task.Delay(new TimeSpan(0, 0, 5, 0), _cancelableSource.Token);
        }
        catch (OperationCanceledException e)
        {
            //This will renew the token, otherwise next Delay call throws exception right away 
            _cancelableSource = new CancellationTokenSource();
        }
    }
}

private void MainTask()
{
    //This will force break the AsyncTask Delay before timeout occurs 
    _cancelableSource.Cancel();
}} 
Visser answered 12/1 at 18:23 Comment(0)
S
1

I often use a utility method which returns false if the delay was cancelled:

public async Task<bool> TryDelay(TimeSpan timeSpan, CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return false;
    }

    try
    {
        await Task.Delay(timeSpan, cancellationToken);
        return true;
    }
    catch (TaskCanceledException)
    {
        return false;
    }
}
Sanskrit answered 28/6 at 4:42 Comment(0)
K
-1
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;
            }
        }
    }
}
Karolekarolina answered 5/12, 2018 at 3:27 Comment(1)
Hey welcome.. You answer may be right but please try to explain your answer instead of simple code dump :)Lorant
M
-1

After running into this problem I wrote a drop in replacement that behaves as expected if you want to do polling:

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

It uses a cancellation token callback to complete a task and then awaits either that synthetic task or the normal Task.Delay with no cancellation token. This way it won't throw an exception when the source token is cancelled, but still responds to the cancellation by returning execution. You still need to check the IsCancellationRequested after calling it to decide what to do if it is cancelled.

Unit tests, if anyone is interested:

    [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));
    }
Miller answered 5/7, 2021 at 22:2 Comment(0)
S
-1
//   while (LoopCheck)
//   {
          for (int i = 0; i < 100 && ! loopCheck; i++)
          {
               textBox1.Text = i.ToString();
               await Task.Delay(1000);
          }
//   }
Scruple answered 28/6 at 5:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.