简体   繁体   中英

Cancelling multiple tasks by registering callbacks on cancellation tokens

I have the following code piece with the output below. I was expecting the second task to be cancelled as it also registers a callback on the cancellation token. But the cancellation only happens on the first task, where the original cancellation was done. Aren't cancellations supposed to be propagated to all token instances? The Microsoft article on Cancellation Tokens does not explain this well.

Any pointers on why this is happening?

Code:

class Program
    {
        static void Main(string[] args)
        {
            AsyncProgramming();
            Console.ReadLine();
        }

        private static async void AsyncProgramming()
        {

            try
            {
                using (var cts = new CancellationTokenSource())
                {
                    var task2 = CreateTask2(cts);
                    var task1 = CreateTask1(cts);

                    Thread.Sleep(5000);
                    await Task.WhenAll(task2, task1);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            Console.WriteLine("Both tasks over");
        }

        private static async Task CreateTask1(CancellationTokenSource cts)
        {
            try
            {
                cts.Token.Register(() => { cts.Token.ThrowIfCancellationRequested(); });
                await Task.Delay(5000);
                Console.WriteLine("This is task one");
                cts.Cancel();
                Console.WriteLine("This should not be printed because the task was cancelled");
            }
            catch (Exception e)
            {
                Console.WriteLine("Task 1 exception: " + e.Message);
                Console.WriteLine("Task 1 was cancelled");
            }

        }

        private static async Task CreateTask2(CancellationTokenSource cts)
        {
            try
            {
                cts.Token.Register(() =>
                {
                    Console.WriteLine("Write something");
                    Thread.CurrentThread.Abort();
                    cts.Token.ThrowIfCancellationRequested();
                });
                await Task.Delay(8000);

                Console.WriteLine("This is task two");
            }
            catch (Exception e)
            {
                Console.WriteLine("Task 2 was cancelled by Task 1");
                Console.WriteLine(e);
            }
        }
    }

Output:

This is task one
Write something
Task 1 exception: Thread was being aborted.
Task 1 was cancelled
This is task two
Thread was being aborted.
Both tasks over

It is not just the second task that fails to cancel. Both registrations to the token work and both ThrowIfCancellationRequested fire, but they are not handled because they run in a different thread.

This happens in the background (twice):

An exception of type 'System.OperationCanceledException' occurred in mscorlib.dll but was not handled in user code

What you should do is call cts.Token.ThrowIfCancellationRequested(); in your function instead of registering to the event.

See the examples at https://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads

Right now you are combining two ways of cancellation: registering to the token cancel event ( Token.Register ), and throwing if the token is cancelled ( Token.ThrowIfCancellationRequested ).

Either you subscribe to the cancel event and perform your own cancel/cleanup logic, or you check in your function code if you should cancel your operation.

An example would look like this:

private static async Task CreateTask2(CancellationToken token)
{
    try
    {
        // Pass on the token when calling other functions.
        await Task.Delay(8000, token);

        // And manually check during long operations.
        for (int i = 0; i < 10000; i++)
        {
            // Do we need to cancel?
            token.ThrowIfCancellationRequested();

            // Simulating work.
            Thread.SpinWait(5000);
        }

        Console.WriteLine("This is task two");
    }
    catch (Exception e)
    {
        Console.WriteLine("Task 2 was cancelled by Task 1");
        Console.WriteLine(e);
    }
}

The first thing is that when you call CancellationToken.Register all it normally does is to store the delegate to call later.

The thread/logic flow calling CancellationTokenSource.Cancel runs all previously registered delegates, regardless of where those were registered from. This means any exception thrown in those normally does not relate in any way to the methods that called Register .

Side note 1: I said normally above, because there is a case where the call to Register will run the delegate right away. I think this is why the msdn documentation is extra confusing. Specifically: if the token was already cancelled, then Register will run the delegate right away, instead of storing it to be ran later. Underneath that happens in CancellationTokenSource.InternalRegister .

The second thing to complete the picture is that all CancellationToken.ThrowIfCancellationRequested does is to throw an exception wherever it is being ran from. That would normally be wherever CancellationTokenSource.Cancel was called from. Note that normally all registered delegates are ran, even if some of those throw an exception.

Side note 2: throwing ThreadAbortException changes the intended logic in the Cancel method, because that special exception can't be caught. When faced with that, cancel stops running any further delegates. The same happens to the calling code, even when catching exceptions.

The last thing to note, is that the presence of the CancellationToken does not affect the logic flow of the methods. All lines in the method run, unless there is code explicitely exiting the method, for example, by throwing an exception. This is what happens if you pass the cancellation token to the Task.Delay calls and it gets cancelled from somewhere else before the time passes. It is also what happens if you were to put calls to CancellationToken.ThrowIfCancellationRequested after specific lines in your method.

Registration of a delegate by Register is just a way to notify when a token goes to the cancelled state, no more. In order to do the cancellation you need to react on this notification in the code and it's mostly needed when execution you want to cancel goes to a stage where cancellation token isn't verified (for example because a method being executed just doesn't accept CancellationToken as paramater) but you still need some control of cancellation state. But in all cases when you deal with executuon of code which has access to CancellationToken you just don't need to subscribe on the cancellation notification.

In your case the first delegate raises exception and this exception is propagated to the Cancel call only that's why the task is cancelled, but this is improper design as you shouldn't deal with CancellationTokenSource in your tasks and shouldn't initiate a cancellation in there, so I'd say that the first cancellation works only by coincidence. For the second task the delegate is invoked but nothing triggers the cancellation inside the task so why should it be cancelled ?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM