Почему задача не отменяется, когда я вызываю метод Cancel CancellationTokenSource в асинхронном методе?

Я создал небольшую оболочку вокруг CancellationToken и CancellationTokenSource. У меня проблема в том, что метод CancelAsync CancellationHelper не работает должным образом.

У меня проблема с методом ItShouldThrowAExceptionButStallsInstead. Чтобы отменить текущую задачу, он вызывает await coordinator.CancelAsync();, но на самом деле задача не отменяется и не генерирует исключение для task.Wait.

ItWorksWellAndThrowsException работает хорошо и использует coordinator.Cancel, который вообще не является асинхронным методом.

Вопрос, почему задача не отменяется, когда я вызываю метод Cancel CancellationTokenSource в асинхронном методе?

Не позволяйте waitHandle сбить вас с толку, это только из-за того, что задание не закончилось раньше.

Пусть код говорит сам за себя:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace TestCancellation
{
    class Program
    {
        static void Main(string[] args)
        {
            ItWorksWellAndThrowsException();
            //ItShouldThrowAExceptionButStallsInstead();
        }

        private static void ItShouldThrowAExceptionButStallsInstead()
        {
            Task.Run(async () =>
            {
                var coordinator = new CancellationHelper();
                var waitHandle = new ManualResetEvent(false);

                var task = Task.Run(() =>
                {
                    waitHandle.WaitOne();

                    //this works well though - it throws
                    //coordinator.ThrowIfCancellationRequested();

                }, coordinator.Token);

                await coordinator.CancelAsync();
                //waitHandle.Set(); -- with or without this it will throw
                task.Wait();
            }).Wait();
        }

        private static void ItWorksWellAndThrowsException()
        {
            Task.Run(() =>
            {
                var coordinator = new CancellationHelper();
                var waitHandle = new ManualResetEvent(false);

                var task = Task.Run(() => { waitHandle.WaitOne(); }, coordinator.Token);

                coordinator.Cancel();
                task.Wait();
            }).Wait();
        }
    }

    public class CancellationHelper
    {
        private CancellationTokenSource cancellationTokenSource;
        private readonly List<Task> tasksToAwait;

        public CancellationHelper()
        {
            cancellationTokenSource = new CancellationTokenSource();
            tasksToAwait = new List<Task>();
        }

        public CancellationToken Token
        {
            get { return cancellationTokenSource.Token; }
        }

        public void AwaitOnCancellation(Task task)
        {
            if (task == null) return;

            tasksToAwait.Add(task);
        }

        public void Reset()
        {
            tasksToAwait.Clear();
            cancellationTokenSource = new CancellationTokenSource();
        }

        public void ThrowIfCancellationRequested()
        {
            cancellationTokenSource.Token.ThrowIfCancellationRequested();
        }

        public void Cancel()
        {
            cancellationTokenSource.Cancel();

            Task.WaitAll(tasksToAwait.ToArray());
        }

        public async Task CancelAsync()
        {
            cancellationTokenSource.Cancel();

            try
            {
                await Task.WhenAll(tasksToAwait.ToArray());
            }
            catch (AggregateException ex)
            {
                ex.Handle(p => p is OperationCanceledException);
            }
        }
    }
}

person Balázs Szántó    schedule 22.06.2015    source источник
comment
Куда ты звонишь CancellationHelper.ThrowIfCancellationRequested()?   -  person Matthew Watson    schedule 22.06.2015
comment
Я считаю, что существует непонимание того, как работает токен отмены... т.е. для Task.Run токен актуален только во время создания задачи, при запуске вы должны проверить токен самостоятельно...   -  person    schedule 22.06.2015
comment
@AndreasNiedermair Я понимаю, о чем вы говорите, но если задача завершается и в какой-то момент во время выполнения вызывается CancellationTokenSource.Cancel, то ее task.Result должно быть TaskStatus.Canceled, не так ли?   -  person Balázs Szántó    schedule 22.06.2015
comment
@AndreasNiedermair Если я правильно понимаю, когда задача начинает выполняться, task.Status не будет TaskStatus.Canceled, даже если вызывается Cancel, и я должен проверить CancellationTokenSource.IsCancellationRequested   -  person Balázs Szántó    schedule 22.06.2015
comment
@boli Может быть шанс для TaskStatus.Faulted, если вы включите token.ThrowIfCancellationRequested()...   -  person    schedule 22.06.2015
comment
Возможный дубликат Как прервать/отменить задачи TPL?   -  person Liam    schedule 20.11.2017


Ответы (1)


Отмена в .Net является кооперативной.

Это означает, что тот, кто содержит CancellationTokenSource, сигнализирует отмену, а тот, кто держит CancellationToken, должен проверить, была ли сигнализирована отмена (либо путем опроса CancellationToken, либо путем регистрации делегата для запуска, когда он сигнализируется).

В вашем Task.Run вы используете CancellationToken в качестве параметра, но вы не проверяете его внутри самой задачи, поэтому задача будет отменена только в том случае, если токен был сигнализирован до того, как задача успела начаться.

Чтобы отменить задание во время его выполнения, необходимо проверить CancellationToken:

var task = Task.Run(() =>
{
    token.ThrowIfCancellationRequested();
}, token);

В вашем случае вы блокируете ManualResetEvent, поэтому вы не сможете проверить CancellationToken. Вы можете зарегистрировать делегата на CancellationToken, который освобождает событие сброса:

token.Register(() => waitHandle.Set())
person i3arnon    schedule 22.06.2015
comment
var task = Task.Run(() =› { token.ThrowIfCancellationRequested(); }, token); выглядит неплохо. Но где ловить исключение. - person swifty; 12.02.2018