Как сделать метод отменяемым, чтобы он не стал уродливым?

В настоящее время я занимаюсь модификацией наших долговременных методов, чтобы их можно было отменить. Я планирую использовать System.Threading.Tasks.CancellationToken для реализации этого.

Наши методы обычно выполняют несколько длительных шагов (в основном отправка команд и затем ожидание оборудования), например.

void Run()
{
    Step1();
    Step2();    
    Step3();
}

Моя первая (может быть, глупая) мысль об отмене превратила бы это в

bool Run(CancellationToken cancellationToken)
{
    Step1(cancellationToken);

    if (cancellationToken.IsCancellationRequested)
        return false;

    Step2(cancellationToken);

    if (cancellationToken.IsCancellationRequested)
        return false;    

    Step3(cancellationToken);

    if (cancellationToken.IsCancellationRequested)
        return false;

    return true;
}

что откровенно выглядит ужасно. Этот «шаблон» будет продолжаться и внутри отдельных шагов (и они обязательно уже довольно длинные). Это сделало бы Thread.Abort() довольно привлекательным, хотя я знаю, что это не рекомендуется.

Существует ли более чистый шаблон для достижения этого, который не прячет логику приложения под большим количеством стандартного кода?

Изменить

В качестве примера характера шагов метод Run может читаться

void Run()
{
    GiantRobotor.MoveToBase();
    Oven.ThrowBaguetteTowardsBase();    
    GiantRobotor.CatchBaguette();
    // ...
}

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


person Jens    schedule 05.11.2013    source источник
comment
Является ли каждый шаг полностью независимым от других шагов?   -  person Matten    schedule 05.11.2013
comment
Не могли бы вы реорганизовать код, чтобы он содержал один метод Step(int number)? Так что вы можете пройти от 1 до 3 и проверить, запрашивается ли токен отмены только один раз?   -  person azz    schedule 05.11.2013
comment
См. stackoverflow.com/questions/3632149/ для всестороннего сравнения различных методов отмены потока.   -  person Markus    schedule 05.11.2013
comment
Не могли бы вы использовать взаимное исключение между вызовом Thread.Abort() и критическими секциями? Тогда вам нужно будет только мьютексировать критический код (возможно, с сохранением состояния) и позволить Thread.Abort() убить все остальное.   -  person neeKo    schedule 05.11.2013
comment
@NikoDraškovic: Это хорошая идея. Достаточно ли этого, чтобы Thread.Abort() больше не был злым? Может быть, опубликовать это как ответ =)   -  person Jens    schedule 05.11.2013
comment
Как можно отменить бросание багета? Потребуется ли вам выполнять действия по очистке в зависимости от того, какие события произошли?   -  person Daniel    schedule 05.11.2013
comment
Моя внутренняя реакция на отменяемое асинхронное действие — рекомендовать Promise объекты, но я не уверен, существует ли существующая реализация, учитывающая многопоточность.   -  person zzzzBov    schedule 06.11.2013
comment
@DanielCook: Ах, правда. Я пытался быть слишком забавным. В моем случае я могу отменить все, и мне не нужна специальная очистка (я думаю).   -  person Jens    schedule 06.11.2013
comment
Это может не сильно помочь, но стоит упомянуть: создайте для этого монаду. Я думаю, вы можете реализовать его на С#. Ваш случай обычно приводится в качестве первого примера в учебниках по монадам (т.е. цепочка функций/шагов, когда можно потерпеть неудачу и нужно прервать цепочку). Посмотрите в сторону Haskell's Maybe Monad. Кажется, хорошее концептуальное соответствие.   -  person CamilB    schedule 06.11.2013
comment
@CamilB: Хорошая идея, спасибо! Вы, конечно, можете сделать это на C#! Эрик Липперт написал в своем блоге красивую серию статей о них. Я попробую и посмотрю, есть ли у этого подхода преимущества перед использованием Task (которые могут быть самими монадами, я не привык с ними работать).   -  person Jens    schedule 06.11.2013


Ответы (8)


Если шаги каким-то образом независимы от потока данных внутри метода, но не могут выполняться параллельно, следующий подход может быть более читаемым:

void Run()
{
    // list of actions, defines the order of execution
    var actions = new List<Action<CancellationToken>>() {
       ct => Step1(ct),
       ct => Step2(ct),
       ct => Step3(ct) 
    };

    // execute actions and check for cancellation token
    foreach(var action in actions)
    {
        action(cancellationToken);

        if (cancellationToken.IsCancellationRequested)
            return false;
    }

    return true;
}

Если шагам не нужен токен отмены, потому что вы можете разделить их на крошечные единицы, вы можете даже написать определение меньшего списка:

var actions = new List<Action>() {
    Step1, Step2, Step3
};
person Matten    schedule 05.11.2013
comment
+1 ПРИМЕЧАНИЕ. Должно быть List<Action<CancellationToken>>, но идея у вас правильная. Я думал о чем-то похожем на это. - person gleng; 05.11.2013
comment
Лично я считаю, что это не очень хорошее решение. Встроенный паттерн ThrowIfCancellationRequested гораздо проще в обслуживании (с потенциально многими вложенными методами) и более удобочитаем. Не говоря уже о том, что фреймворковые/внешние методы, которые принимают CancellationToken, скорее всего, уже будут генерировать OperationCanceledExceptions, что означает, что вам все равно придется обернуть свой метод Run в блок try catch. Я добавил ответ ниже. - person Andrew Hanlon; 12.11.2013
comment
@AndrewHanlon Вызывающий абонент также может поймать OperationCanceledException, или вы можете просто обернуть часть выполнения. Не вижу связи вашего комментария с моим ответом. Это зависит от способа реализации методов. - person Matten; 13.11.2013

как насчет продолжения?

var t = Task.Factory.StartNew(() => Step1(cancellationToken), cancellationToken)
   .ContinueWith(task => Step2(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current)
   .ContinueWith(task => Step3(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
person Nickolai Nielsen    schedule 05.11.2013
comment
Хороший ответ, это то, в чем Задачи преуспевают. - person Bryan Boettcher; 05.11.2013
comment
Но это выглядит очень некрасиво! Очень удобный для машин, но едва читаемый. - person Harry; 01.11.2016

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

if (cancellationToken.IsCancellationRequested) { /* Stop */ }

...или немного короче:

cancellationToken.ThrowIfCancellationRequested()

Как правило, если вы можете передать токен отмены отдельным шагам, вы можете распределить проверки таким образом, чтобы они не переполняли код. Вы также можете не проверять отмену постоянно; если операции, которые вы выполняете, являются идемпотентными и нетребовательными к ресурсам, вам не обязательно проверять отмену на каждом этапе. Самое важное время для проверки — перед возвратом результата.

Если вы передаете токен на все свои шаги, вы можете сделать что-то вроде этого:

public static CancellationToken VerifyNotCancelled(this CancellationToken t) {
    t.ThrowIfCancellationRequested();
    return t;
}

...

Step1(token.VerifyNotCancelled());
Step2(token.VerifyNotCancelled());
Step3(token.VerifyNotCancelled());
person Mike Strobel    schedule 05.11.2013
comment
Кроме того, проверяйте отмену после повторения важных элементов данных (например, каждого файла в операции копирования файлов) и только тогда, когда вы можете гарантировать успешную очистку. - person Gusdor; 05.11.2013
comment
В моем случае эти операции требуют больших ресурсов, но, тем не менее, это хороший совет. Спасибо =) - person Jens; 05.11.2013

Когда мне нужно было сделать что-то подобное, я создал для этого делегата:

bool Run(CancellationToken cancellationToken)
{
    var DoIt = new Func<Action<CancellationToken>,bool>((f) =>
    {
        f(cancellationToken);
        return cancellationToken.IsCancellationRequested;
    });

    if (!DoIt(Step1)) return false;
    if (!DoIt(Step2)) return false;
    if (!DoIt(Step3)) return false;

    return true;
}

Или, если между шагами никогда нет кода, вы можете написать:

return DoIt(Step1) && DoIt(Step2) && DoIt(Step3);
person Jim Mischel    schedule 05.11.2013

Укороченная версия:

Используйте lock() для синхронизации вызова Thread.Abort() с критическими секциями.


Поясню версию:

Как правило, при прерывании потока вам придется учитывать два типа кода:

  • Длинный код, вам все равно, завершится ли он
  • Код, который обязательно должен идти своим чередом

Первый тип — это тип, который нас не волнует, если пользователь запросил прерывание. Может быть, мы считали до ста миллиардов, и ему уже все равно?

Если бы мы использовали часовых, таких как CancellationToken, мы вряд ли стали бы тестировать их в каждой итерации неважного кода, не так ли?

for(long i = 0; i < bajillion; i++){
    if(cancellationToken.IsCancellationRequested)
        return false;
    counter++;
}

Такой страшный. Так что для этих случаев Thread.Abort() - находка.

К сожалению, как говорят некоторые, вы не можете использовать Thread.Abort() из-за атомарного кода, который обязательно должен работать! Код уже снял деньги с вашего счета, теперь он должен завершить транзакцию и перевести деньги на целевой счет. Никто не любит исчезновение денег.

К счастью, у нас есть взаимное исключение, чтобы помочь нам с такими вещами. И C# делает это красиво:

//unimportant long task code
lock(_lock)
{
    //atomic task code
}

И в другом месте

lock(_lock) //same lock
{
    _thatThread.Abort();
}

Количество блокировок всегда будет равно <= числу дозорных, потому что вам также нужны дозорные для неважного кода (чтобы ускорить его прерывание). Это делает код немного красивее, чем в версии Sentinel, но также улучшает прерывание, потому что ему не нужно ждать неважных вещей.


Также следует отметить, что ThreadAbortException можно поднять где угодно, даже в finally блоках. Эта непредсказуемость делает Thread.Abort() таким противоречивым.

С блокировками этого можно избежать, просто заблокировав весь блок try-catch-finally. Если очистка в finally необходима, тогда можно заблокировать весь блок. Блоки try обычно делаются максимально короткими, предпочтительно одной строкой, поэтому мы не блокируем ненужный код.

Это делает Thread.Abort(), как вы говорите, немного менее злым. Однако вы можете не захотеть вызывать его в потоке пользовательского интерфейса, поскольку теперь вы его блокируете.

person neeKo    schedule 05.11.2013

Я немного удивлен, что никто не предложил стандартный, встроенный способ справиться с этим:

bool Run(CancellationToken cancellationToken)
{        
    //cancellationToke.ThrowIfCancellationRequested();

    try
    {
        Step1(cancellationToken);
        Step2(cancellationToken);
        Step3(cancellationToken);
    }
    catch(OperationCanceledException ex)
    {
        return false;
    }

    return true;
}

void Step1(CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    ...
}

Хотя обычно вы не хотите полагаться на отправку проверок на более глубокие уровни, в этом случае шаги уже принимают CancellationToken и должны иметь проверку в любом случае (как и любой нетривиальный метод, принимающий CancellationToken).

Это также позволяет вам быть гранулированными или негранулярными с проверками по мере необходимости, то есть перед длительными/интенсивными операциями.

person Andrew Hanlon    schedule 11.11.2013

Если рефакторинг допустим, вы можете реорганизовать методы Step таким образом, чтобы был один метод Step(int number).

Затем вы можете перейти от 1 к N и проверить, запрашивается ли токен отмены только один раз.

bool Run(CancellationToken cancellationToken) {
    for (int i = 1; i < 3 && !cancellationToken.IsCancellationRequested; i++) 
        Step(i, cancellationToken);

    return !cancellationToken.IsCancellationRequested;
}

Или, что то же самое: (Как вы предпочитаете)

bool Run(CancellationToken cancellationToken) {
    for (int i = 1; i < 3; i++) {
        Step(i, cancellationToken);
        if (cancellationToken.IsCancellationRequested)
            return false;
    }
    return true;
}
person azz    schedule 05.11.2013
comment
Я считаю, что возвращаемое значение также должно быть инвертировано. - person Mike Strobel; 05.11.2013

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

person Archagon    schedule 05.11.2013