Элегантно обрабатывать отмену задачи

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

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (OperationCanceledException)
    {
        throw;
    }
    catch (Exception ex)
    {
        Log.Exception(ex);
        throw;
    }
}

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

Это всегда казалось немного неуклюжим, и визуальная студия по умолчанию разрывает бросок для OperationCanceledException (хотя у меня есть "break on User-unhandled", отключенный теперь для OperationCanceledException из-за моего использования этого шаблона).

В идеале я думаю, что хотел бы сделать что-то вроде этого:

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (Exception ex) exclude (OperationCanceledException)
    {
        Log.Exception(ex);
        throw;
    }
}

то есть. имеют какой-то список исключений, применяемый к catch, но без поддержки языка, который в настоящее время невозможен (@eric-lippert: С# vNext:)).

Другим способом будет продолжение:

public void StartWork()
{
    Task.Factory.StartNew(() => DoWork(cancellationSource.Token), cancellationSource.Token)
        .ContinueWith(t => Log.Exception(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
}

public void DoWork(CancellationToken cancelToken)
{
    //do work
    cancelToken.ThrowIfCancellationRequested();
    //more work
}

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

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

Мне просто нужно придерживаться примера 1?

Эамон

Ответ 1

Итак, в чем проблема? Просто отбросьте блок catch (OperationCanceledException) и установите правильные продолжения:

var cts = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
    {
        var i = 0;
        try
        {
            while (true)
            {
                Thread.Sleep(1000);

                cts.Token.ThrowIfCancellationRequested();

                i++;

                if (i > 5)
                    throw new InvalidOperationException();
            }
        }
        catch
        {
            Console.WriteLine("i = {0}", i);
            throw;
        }
    }, cts.Token);

task.ContinueWith(t => 
        Console.WriteLine("{0} with {1}: {2}", 
            t.Status, 
            t.Exception.InnerExceptions[0].GetType(), 
            t.Exception.InnerExceptions[0].Message
        ), 
        TaskContinuationOptions.OnlyOnFaulted);

task.ContinueWith(t => 
        Console.WriteLine(t.Status), 
        TaskContinuationOptions.OnlyOnCanceled);

Console.ReadLine();

cts.Cancel();

Console.ReadLine();

TPL отличает отмену и ошибку. Следовательно, аннулирование (т.е. Бросание OperationCancelledException внутри тела задачи) не является ошибкой.

Главное: не обрабатывать исключения внутри тела задачи, не перебрасывая их.

Ответ 2

Вот как вы элегантно обрабатываете отмену задачи:

Работа с задачами "огонь и забвение"

var cts = new CancellationTokenSource( 5000 );  // auto-cancel in 5 sec.
Task.Run( () => {
    cts.Token.ThrowIfCancellationRequested();

    // do background work

    cts.Token.ThrowIfCancellationRequested();

    // more work

}, cts.Token ).ContinueWith( task => {
    if ( !task.IsCanceled && task.IsFaulted )   // suppress cancel exception
        Logger.Log( task.Exception );           // log others
} );

Обработка ожидания Ожидание завершения задачи/аннулирование

var cts = new CancellationTokenSource( 5000 ); // auto-cancel in 5 sec.
var taskToCancel = Task.Delay( 10000, cts.Token );  

// do work

try { await taskToCancel; }           // await cancellation
catch ( OperationCanceledException ) {}    // suppress cancel exception, re-throw others

Ответ 3

С# 6.0 имеет решение для этого.. Фильтрация исключений

int denom;

try
{
     denom = 0;
    int x = 5 / denom;
}

// Catch /0 on all days but Saturday

catch (DivideByZeroException xx) when (DateTime.Now.DayOfWeek != DayOfWeek.Saturday)
{
     Console.WriteLine(xx);
}

Ответ 4

Согласно этому сообщению в блоге MSDN, вы должны поймать OperationCanceledException, например

async Task UserSubmitClickAsync(CancellationToken cancellationToken)
{
   try
   {
      await SendResultAsync(cancellationToken);
   }
   catch (OperationCanceledException) // includes TaskCanceledException
   {
      MessageBox.Show('Your submission was canceled.');
   }
}

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

async Task SendResultAsync(CancellationToken cancellationToken)
{
   try
   {
      await httpClient.SendAsync(form, cancellationToken);
   }
   catch (OperationCanceledException)
   {
      // perform your cleanup
      form.Dispose();

      // rethrow exception so caller knows youve canceled.
      // DONT 'throw ex;' because that stomps on 
      // the Exception.StackTrace property.
      throw; 
   }
}

Ответ 5

Вы могли бы сделать что-то вроде этого:

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (OperationCanceledException) when (cancelToken.IsCancellationRequested)
    {
        throw;
    }
    catch (Exception ex)
    {
        Log.Exception(ex);
        throw;
    }
}

Ответ 6

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

public void DoWork(CancellationToken cancelToken)
{
    try
    {
        //do work
        cancelToken.ThrowIfCancellationRequested();
        //more work
    }
    catch (OperationCanceledException) {}
    catch (Exception ex)
    {
        Log.Exception(ex);
    }
}

Возможно, вы заметили, что я удалил инструкцию throw отсюда. Это не вызовет исключения, а просто проигнорирует его.

Сообщите мне, если вы намерены сделать что-то еще.

Существует еще один способ, который очень близок к тому, что вы показали в своем коде

    catch (Exception ex)
    {
        if (!ex.GetType().Equals(<Type of Exception you don't want to raise>)
        {
            Log.Exception(ex);

        }
    }