4

I play with cancelation token, and I would like to understand how this works. I have two async methods(in my example two but in theory I can have 100). I want to cancel work in all async methods if one of them throws an exception.

My idea is to cancel token in exception where all methods are called. When a token is canceled I would expect that other method stop working, but this is not happening.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace CancelationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            new Test();
            Console.ReadKey();
        }
    }

    public class Test
    {
        public Test()
        {
            Task.Run(() => MainAsync());
        }

        public static async Task MainAsync()
        {
            var cancellationTokenSource = new CancellationTokenSource();
            try
            {
                var firstTask = FirstAsync(cancellationTokenSource.Token);
                var secondTask = SecondAsync(cancellationTokenSource.Token);
                Thread.Sleep(50);
                Console.WriteLine("Begin");
                await secondTask;
                Console.WriteLine("hello");
                await firstTask;
                Console.WriteLine("world");
                Console.ReadKey();
            }
            catch (OperationCanceledException e)
            {
                Console.WriteLine("Main OperationCanceledException cancel");
            }
            catch (Exception e)
            {
                Console.WriteLine("Main Exception + Cancel");
                cancellationTokenSource.Cancel();
            }
        }

        public static async Task FirstAsync(CancellationToken c)
        {
            c.ThrowIfCancellationRequested();
            await Task.Delay(1000, c);
            Console.WriteLine("Exception in first call");
            throw new NotImplementedException("Exception in first call");
        }

        public static async Task SecondAsync(CancellationToken c)
        {
            c.ThrowIfCancellationRequested();
            await Task.Delay(15000, c);
            Console.WriteLine("SecondAsync is finished");
        }
    }
}

The second method finish work and delay task for 15 seconds even when the first method throws an exception.

What is result:

Begin

Exception in the first call

SecondAsync is finished

hello

Main Exception + Cancel

I would expect that secondAsync stop delay and throw OperationCancelException. I would expect this result:

Begin

Exception in first call

Main Exception + Cancel

Main OperationCanceledException cancel

Where am I making mistake? Why method SecondAsync is fully executed and doesn't throw an exception? And if I change the order of SecondAsync and FirstAsync than Second method stop to delay when the token is canceled and throw an exception.

Raskolnikov
  • 3,791
  • 9
  • 43
  • 88
  • 5
    Cancelation is cooperative: your Tasks have to check the token regularly and that requires a loop. – H H Mar 18 '18 at 12:24
  • @HenkHolterman OP passing `CancellationToken` to `Task.Delay`. Should delay be cancelled when token signaled? – user4003407 Mar 18 '18 at 12:32
  • @PetSerAl - yes, I overlooked that. Although the docs are a bit vague about when Delay cancels, I think it might still take 15 secs here. – H H Mar 18 '18 at 12:44
  • 2
    @Raskolnikov Cancellation doesn't work in a way that allows to 'subscribe to token cancelled event' as you probably expect it to.. The `ThrowIfCancellationRequested` simply check if token cancellation *was* requested and if yes - throws an exception, but if it's not - it does nothing. It doesn't throw if it's cancelled at a later stage. – Fabjan Mar 18 '18 at 12:46
  • You can also [register a Delegate](https://learn.microsoft.com/en-us/dotnet/standard/threading/how-to-register-callbacks-for-cancellation-requests) to run when cancellation is requested. – Crowcoder Mar 18 '18 at 13:02
  • @Fabian are you sure about this? If I change order of FirstAsync and SeconAsync it working like that. Delay is stoped. – Raskolnikov Mar 18 '18 at 14:58
  • @Fabian What i point of passing token to delay function? – Raskolnikov Mar 18 '18 at 15:00
  • Try to await the tasks together, not sequentially – VMAtm Mar 18 '18 at 16:01

3 Answers3

4

Because the relevant part of your code is:

try
{
   ...
   await secondTask;
   await firstTask;
}
catch(...)
{
    source.Cancel();
}

Now while the firstTask is started and has thrown, it is awaited after the secondTask. The exception won't surface in the caller until the task is awaited. And so the catch clause will only execute after secondTask has already completed. The Cancel() is just happening too late.

If you want your firstTask to interrupt the second one you will have to

  1. pass the source into FirstAsync and call Cancel() there. A little ugly.
  2. change the await structure. I think your sample is a little artificial. Use Parallel.Invoke() or something similar and it will happen quite naturally.
H H
  • 263,252
  • 30
  • 330
  • 514
  • How exactly does this answer OPs question. Quote: 'I would expect that other method stop working, but this is not happening' ? – Fabjan Mar 18 '18 at 12:43
  • It answers that completely. And compactly. Go through the code. – H H Mar 18 '18 at 12:45
  • I mean that the answer is too short and it'd be nice if you add more explanation regarding 'why other method stops working'... Maybe I'm wrong but imho OP wants to find an answer to why method `SecondAsync` is fully executed and doesn't throw exception in `ThrowIfCancellationRequested` – Fabjan Mar 18 '18 at 12:49
  • I don't know what th OP think about ThrowIfCancellationRequested, and it doesn't really matter here. – H H Mar 18 '18 at 12:54
  • How do you use `Parallel.Invoke` with `async` methods? – svick Mar 18 '18 at 13:58
  • They wouldn't (need to) be async. You're right that that doesn't mix well. I was addressing the Canellation. – H H Mar 18 '18 at 14:04
  • Why method SecondAsync is fully executed and doesn't throw an exception ? And if I change the order of SecondAsync and FirstAsync than Second method stop to delay when the token is canceled and throw an exception.. – Raskolnikov Mar 18 '18 at 15:10
  • Even if an exception is thrown when await, still function should cancel delay. What would be the point of passing token to delay function? – Raskolnikov Mar 18 '18 at 15:15
  • After some thinking, I see your point. "SecondAsync would not be fully executed if the token was canceled. But token is not canceled since cancellation is done in exception catch, and the exception was never thrown since throw is done on await." If I am correct please add this explanation to your answer. – Raskolnikov Mar 18 '18 at 15:35
  • Why do you think that would be ugly to pass source to FirstAsync? If passing token is not ugly why would be passing source instead of token? – Raskolnikov Mar 18 '18 at 15:47
  • Yes, the token is cancelled from the catch clause, I edited a little. – H H Mar 18 '18 at 17:07
  • Well, it's 'ugly' because the firstTask is one of many clients of the token, it should not be so much in control of the overall picture. But it's not a big deal and it might be the best solution here. Notice you will need a try/catch in FirstAsync then. – H H Mar 18 '18 at 17:11
0

Your CancellationTokenSource needs to be in a scope that is accessible to the methods being called, and you need to call CancellationTokenSource.Cancel() in order to cancel all of the operations using that source.

You can also call CancellationTokenSource.CancelAfter(TimeSpan) to cancel it on a delay.

IAmJersh
  • 742
  • 8
  • 25
0

The cancellation must be checked on manually. You only do this once by calling c.ThrowIfCancellationRequested(); which is before the delay. So you will not register any cancellation once the delay started.

Instead, you should rather cyclically check for the cancellation in a loop with whatever loop time is apropiate for your application:

        public static async Task SecondAsync(CancellationToken c)
        {
            for (int i = 0; i<150; i++)
            {
                c.ThrowIfCancellationRequested();
                await Task.Delay(100, c);
            }
            c.ThrowIfCancellationRequested();
            Console.WriteLine("SecondAsync is finished");
        }

In production code you might want to insert the cancellation check every now and then if you have a very linear workflow.

Edit: Also, as Henk Holterman pointed out in their answer, the exception propagated through await firstAsync cannot be catched before await secondAsnc finished in your code.

Felix
  • 1,066
  • 1
  • 5
  • 22