Как сделать метод отмененным без его уродства? - программирование
Подтвердить что ты не робот

Как сделать метод отмененным без его уродства?

В настоящее время я переоборудовал наши длительные методы для отмены. Я планирую использовать 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() выглядеть довольно сексуально, хотя я знаю, что это не рекомендуется.

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

Edit

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

Краткая версия:

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


Позвольте мне пояснить версию:

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Ответ 6

Если рефактору разрешено, вы можете реорганизовать методы 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;
}

Ответ 7

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

Ответ 8

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

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).

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