41

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?

gitesh.tyagi
  • 2,271
  • 1
  • 14
  • 21
Zgrkpnr
  • 1,191
  • 4
  • 15
  • 32
  • 1
    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. – Smeegs Apr 04 '14 at 20:45
  • Exactely. But as you can admit, documantation about this is very new and not enough. Maybe someone here knows how to use cancellationtoken – Zgrkpnr Apr 04 '14 at 20:46
  • 3
    Actually, the [cancellation documentation](http://msdn.microsoft.com/en-us/library/dd997364(v=vs.110).aspx) is quite exhaustive and has been up almost four years now. – Stephen Cleary Apr 04 '14 at 20:50
  • [Avoid async void](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void). – Theodor Zoulias Sep 26 '20 at 09:05

5 Answers5

57

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();
James
  • 80,725
  • 18
  • 167
  • 237
  • 6
    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. – Zgrkpnr Apr 04 '14 at 21:31
  • 9
    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. – WhoIsRich Apr 04 '14 at 23:14
  • 3
    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++)` – Jeremy Lakeman Mar 16 '20 at 01:50
  • 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()`. – Theodor Zoulias Sep 25 '20 at 07:13
  • 5
    @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. – James Sep 25 '20 at 07:58
  • 3
    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. – Theodor Zoulias Sep 25 '20 at 08:23
  • @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... – James Sep 26 '20 at 07:59
  • 1
    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. – Theodor Zoulias Sep 26 '20 at 08:56
  • 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 – James Sep 26 '20 at 18:31
  • Nahh, I will prefer to limit my involvement at reversing my vote, after the problems have been fixed. – Theodor Zoulias Sep 27 '20 at 07:01
  • @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. – James Sep 27 '20 at 08:20
  • James if you want to incentivise people to improve your answers, you could mark your posts as [Community Wikis](https://meta.stackexchange.com/questions/11740/what-are-community-wiki-posts). This feature prevents the work of one member to unfairly increase the reputation of another member. – Theodor Zoulias Sep 27 '20 at 09:01
  • @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 readers – James Sep 27 '20 at 09:29
  • There was no point at posting my own answer since this question had already an excellent answer (Stephen Cleary's [answer](https://stackoverflow.com/a/22872806/11178549)). 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 . – Theodor Zoulias Sep 27 '20 at 09:43
11

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
9

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
            }
        }
    }
}
The Lemon
  • 1,211
  • 15
  • 26
  • 2
    Also you might want to catch OperationCanceledException because you will catch all types of Canceled Exceptions, not just the TaskCanceledExceptions. – Daniel James Bryars Aug 04 '20 at 07:14
  • It is bad. DoSomething... can throw `OperationCanceledException` too. It should be: `catch (TaskCanceledException tce) when (tce.CancellationToken == token)` – apocalypse Feb 16 '22 at 17:03
  • @apocalypse 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. – The Lemon Feb 17 '22 at 22:30
0

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));
    }
Drew Delano
  • 1,421
  • 16
  • 21
-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
    Hey welcome.. You answer may be right but please try to explain your answer instead of simple code dump :) – Ali Dec 05 '18 at 03:47