Есть ли правильный способ отменить асинхронную задачу?

Я столкнулся с проблемой, как правильно отменить асинхронную задачу.

Вот черновик.

Моя точка входа выполняет две асинхронные задачи. Первая задача выполняет «долгую» работу, а вторая ее отменяет.

Точка входа:

private static void Main()
{
    var ctc = new CancellationTokenSource();

    var cancellable = ExecuteLongCancellableMethod(ctc.Token);

    var cancelationTask = Task.Run(() =>
    {
        Thread.Sleep(2000);

        Console.WriteLine("[Before cancellation]");

        ctc.Cancel();
    });

    try
    {
        Task.WaitAll(cancellable, cancelationTask);
    }
    catch (Exception e)
    {
        Console.WriteLine($"An exception occurred with type {e.GetType().Name}");
    }
}

Метод, возвращающий задачу, которую можно отменить:

private static Task ExecuteLongCancellableMethod(CancellationToken token)
{
    return Task.Run(() =>
    {
        token.ThrowIfCancellationRequested();

        Console.WriteLine("1st"); 
        Thread.Sleep(1000);

        Console.WriteLine("2nd");
        Thread.Sleep(1000);

        Console.WriteLine("3rd");
        Thread.Sleep(1000);

        Console.WriteLine("4th");
        Thread.Sleep(1000);

        Console.WriteLine("[Completed]");

    }, token);  
}   

Моя цель - перестать писать «1-й», «2-й», «3-й» сразу после вызова отмены. Но я получаю следующие результаты:

1st
2nd
3rd
[Before cancellation]
4th
[Completed]

По очевидной причине у меня не было исключения, которое выдает при запросе отмены. Поэтому я попытался переписать метод следующим образом:

private static Task ExecuteLongCancellableAdvancedMethod(CancellationToken token)
{
    return Task.Run(() =>
    {
        var actions = new List<Action>
        {
            () => Console.WriteLine("1st"),
            () => Console.WriteLine("2nd"),
            () => Console.WriteLine("3rd"),
            () => Console.WriteLine("4th"),
            () => Console.WriteLine("[Completed]")
        };

        foreach (var action in actions)
        {
            token.ThrowIfCancellationRequested();

            action.Invoke();

            Thread.Sleep(1000);
        }

    }, token);
}

И теперь я получил то, что хочу:

1st
2nd
[Before cancellation]
3rd
An exception occurred with type AggregateException

Но я полагаю, что создание коллекции делегатов Action и их выполнение в цикле - не самый удобный способ решения моей проблемы.

Итак, как это правильно делать? И зачем мне передавать свой токен отмены в метод Task.Run в качестве второго аргумента?


person Vladyslav Yefremov    schedule 29.08.2017    source источник
comment
Если отмена токена уже была запрошена, Task.Run вообще не будет запускать делегат   -  person Jakub Dąbek    schedule 29.08.2017
comment
Вам нужно явно вызывать token.ThrowIfCancellationRequested(); в каждом месте, где вы хотите, чтобы выполнение действительно могло быть отменено. Итак, в вашем первом примере перед каждым Console.WriteLine.   -  person Bradley Uffner    schedule 29.08.2017
comment
Я настоятельно рекомендую вам ознакомиться с методом ContinueWith, так как вы спросили о правильном способе, я думаю, вы захотите познакомиться с ContinueWith и TaskContinuationOptions.NotOnCanceled. Вот ссылка с дополнительной полезной информацией: social.msdn.microsoft.com/Forums/en-US/   -  person Jace    schedule 29.08.2017


Ответы (1)


Task не отменяет сам себя, вы должны определить запрос на отмену и полностью прервать свою работу. Это то, что делает token.ThrowIfCancellationRequested();.

Вы должны размещать эти проверки по всему вашему коду, в местах, где выполнение может быть полностью остановлено или откатано до безопасного состояния.

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

Если вы измените его так, чтобы он выглядел так, вы также увидите ожидаемые результаты.

return Task.Run(() =>
{
    token.ThrowIfCancellationRequested();
    Console.WriteLine("1st"); 
    Thread.Sleep(1000);

    token.ThrowIfCancellationRequested();
    Console.WriteLine("2nd");
    Thread.Sleep(1000);

    token.ThrowIfCancellationRequested();
    Console.WriteLine("3rd");
    Thread.Sleep(1000);

    token.ThrowIfCancellationRequested();
    Console.WriteLine("4th");
    Thread.Sleep(1000);

    Console.WriteLine("[Completed]");

}, token); 
person Bradley Uffner    schedule 29.08.2017
comment
Спасибо, я знаю, как работает написанный мной код, но есть ли более удобные способы отменить задачу? Я имею в виду писать ThrowIfCancellationRequested после каждого логического блока, чтобы код выглядел ужасно, как для меня - person Vladyslav Yefremov; 29.08.2017
comment
Нет, насколько я знаю. Только разработчик может знать, где в коде есть безопасные места, чтобы полностью отменить Task. Код в Task мог бы оставить переменные и данные в нечистом, неизвестном состоянии, если бы он каким-то образом отменил, как только будет запрошена отмена, и это может быть так же опасно, как и Thread.Abort. Только представьте себе последствия, если Task где взять блокировку объекта, и она была отменена до того, как ее можно было снять, или если Задача имела дело с неуправляемой памятью - person Bradley Uffner; 29.08.2017