Отмена нескольких задач путем регистрации обратных вызовов на токенах отмены - PullRequest
2 голосов
/ 16 мая 2019

У меня есть следующий фрагмент кода с выводом ниже.Я ожидал, что вторая задача будет отменена, поскольку она также регистрирует обратный вызов на маркере отмены.Но отмена происходит только на первом задании, где была выполнена первоначальная отмена.Разве отмены не должны распространяться на все экземпляры токенов?Статья Microsoft о токенах отмены не очень хорошо объясняет это.

Есть ли какие-нибудь указатели на то, почему это происходит?

Код:

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

Вывод:

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

Ответы [ 3 ]

2 голосов
/ 16 мая 2019

Это не просто вторая задача, которую не удалось отменить.Обе регистрации на токене работают, и оба ThrowIfCancellationRequested срабатывают, но они не обрабатываются, потому что они работают в другом потоке.

Это происходит в фоновом режиме (дважды):

Исключение типа 'System.OperationCanceledException' произошло в mscorlib.dll, но не было обработано в коде пользователя

Что вам нужно сделать, это вызвать cts.Token.ThrowIfCancellationRequested(); в вашей функции вместо регистрации на событие.

См. Примеры на https://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads

Сейчас вы комбинируете два способа отмены: регистрация на событие отмены токена (Token.Register) и бросание, если токен отменен (Token.ThrowIfCancellationRequested).

Либо вы подписываетесь на событие отмены и выполняете свою собственную логику отмены / очистки, либо вы проверяете свой код функции, если вам следует отменить свою операцию.

Пример может выглядеть следующим образом:

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);
    }
}
1 голос
/ 20 мая 2019

Во-первых, когда вы вызываете CancellationToken.Register, все, что он обычно делает, это сохраняет делегат для вызова позже.

Поток / логический поток, вызывающий CancellationTokenSource.Cancel, запускает все ранее зарегистрированные делегаты, независимо ототкуда те были зарегистрированы.Это означает, что любое исключение, выдаваемое в тех, которые обычно не связаны, никак не связано с методами, которые вызывали Регистр .

Примечание 1: Я сказал обычно выше, потому что есть случай, когда вызов Register сразу же запустит делегата.Я думаю, именно поэтому документация msdn очень запутана.В частности: если токен уже был отменен, Register сразу же запустит делегат, а не сохранит его для запуска позже.Внизу, что происходит в CancellationTokenSource.InternalRegister.

Второе, что завершает картину, это то, что все, что CancellationToken.ThrowIfCancellationRequested делает, - это генерирует исключение, откуда бы оно ни запускалось.Обычно это было бы там, откуда CancellationTokenSource.Cancel был вызван.Обратите внимание, что обычно все зарегистрированные делегаты запускаются, даже если некоторые из них выдают исключение.

Примечание 2: throwing ThreadAbortException изменяет предполагаемую логику в методе Cancel, потому чтоэто особое исключение не может быть поймано.Когда сталкиваешься с этим, отмена прекращает работу дальнейших делегатов.То же самое происходит с вызывающим кодом, даже при перехвате исключений.

Последнее, на что нужно обратить внимание, это то, что наличие CancellationToken не влияет на логический поток методов.Все строки в методе выполняются, если только нет явного выхода из метода, например, с помощью исключения.Это то, что происходит, если вы передаете токен отмены вызовам Task.Delay, и он отменяется откуда-то еще до истечения времени.Это также то, что происходит, если вы должны были вызывать CancellationToken.ThrowIfCancellationRequested после определенных строк в вашем методе.

0 голосов
/ 16 мая 2019

Регистрация делегата по Register - это просто способ уведомления о переходе токена в отмененное состояние, не более. Чтобы выполнить отмену, вам нужно отреагировать на это уведомление в коде, и это в основном необходимо, когда выполнение, которое вы хотите отменить, переходит на этап, где токен отмены не проверен (например, потому что выполняемый метод просто не принимает CancellationToken как параметр), но вам все еще нужен некоторый контроль состояния отмены. Но во всех случаях, когда вы имеете дело с выполнением кода, который имеет доступ к CancellationToken, вам просто не нужно подписываться на уведомление об отмене.

В вашем случае первый делегат вызывает исключение, и это исключение распространяется только на вызов Cancel, поэтому задача отменяется, но это неправильный дизайн, поскольку вы не должны иметь дело с CancellationTokenSource в своих задачах и не должны не инициировать отмену там, поэтому я бы сказал, что первая отмена работает только по стечению обстоятельств. Для второй задачи делегат вызывается, но ничто не вызывает отмены внутри задачи, так почему его следует отменить?

...