Como cancelar uma tarefa em espera?

164

Estou brincando com essas tarefas do Windows 8 WinRT e estou tentando cancelar uma tarefa usando o método abaixo e funciona até certo ponto. O método CancelNotification é chamado, o que faz você pensar que a tarefa foi cancelada, mas, em segundo plano, a tarefa continua sendo executada; depois de concluída, o status da tarefa é sempre concluído e nunca é cancelado. Existe uma maneira de interromper completamente a tarefa quando ela é cancelada?

private async void TryTask()
{
    CancellationTokenSource source = new CancellationTokenSource();
    source.Token.Register(CancelNotification);
    source.CancelAfter(TimeSpan.FromSeconds(1));
    var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token);

    await task;            

    if (task.IsCompleted)
    {
        MessageDialog md = new MessageDialog(task.Result.ToString());
        await md.ShowAsync();
    }
    else
    {
        MessageDialog md = new MessageDialog("Uncompleted");
        await md.ShowAsync();
    }
}

private int slowFunc(int a, int b)
{
    string someString = string.Empty;
    for (int i = 0; i < 200000; i++)
    {
        someString += "a";
    }

    return a + b;
}

private void CancelNotification()
{
}
Carlo
fonte
Acabei de encontrar este artigo que me ajudou a entender as várias maneiras de cancelar.
Uwe Keim

Respostas:

239

Leia-se sobre cancelamento (que foi introduzido no .NET 4.0 e é em grande parte inalterado desde então) eo padrão assíncrono baseado em tarefas , que fornece orientações sobre como usar CancellationTokencom asyncmétodos.

Para resumir, você passa um CancellationTokenpara cada método que suporta cancelamento, e esse método deve verificá-lo periodicamente.

private async Task TryTask()
{
  CancellationTokenSource source = new CancellationTokenSource();
  source.CancelAfter(TimeSpan.FromSeconds(1));
  Task<int> task = Task.Run(() => slowFunc(1, 2, source.Token), source.Token);

  // (A canceled task will raise an exception when awaited).
  await task;
}

private int slowFunc(int a, int b, CancellationToken cancellationToken)
{
  string someString = string.Empty;
  for (int i = 0; i < 200000; i++)
  {
    someString += "a";
    if (i % 1000 == 0)
      cancellationToken.ThrowIfCancellationRequested();
  }

  return a + b;
}
Stephen Cleary
fonte
2
Uau, ótima informação! Isso funcionou perfeitamente, agora eu preciso descobrir como lidar com a exceção no método assíncrono. Valeu cara! Vou ler as coisas que você sugeriu.
Carlo
8
Não. A maioria dos métodos síncronos de longa duração tem alguma maneira de cancelá-los - às vezes fechando um recurso subjacente ou chamando outro método. CancellationTokenpossui todos os ganchos necessários para interoperar com sistemas de cancelamento personalizados, mas nada pode cancelar um método não cancelável.
Stephen Cleary
1
Ah entendo. Portanto, a melhor maneira de capturar a ProcessCancelledException é agrupar o 'aguardar' em uma tentativa / captura? Às vezes, recebo o AggregatedException e não consigo lidar com isso.
Carlo
3
Certo. Eu recomendo que você nunca use Waitou Resultem asyncmétodos; você sempre deve usar await, que desembrulha a exceção corretamente.
precisa
11
Apenas curioso, há uma razão pela qual nenhum dos exemplos usa CancellationToken.IsCancellationRequestede sugere sugestões de exceções?
James M
41

Ou, para evitar modificações slowFunc(digamos que você não tenha acesso ao código fonte, por exemplo):

var source = new CancellationTokenSource(); //original code
source.Token.Register(CancelNotification); //original code
source.CancelAfter(TimeSpan.FromSeconds(1)); //original code
var completionSource = new TaskCompletionSource<object>(); //New code
source.Token.Register(() => completionSource.TrySetCanceled()); //New code
var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token); //original code

//original code: await task;  
await Task.WhenAny(task, completionSource.Task); //New code

Você também pode usar bons métodos de extensão em https://github.com/StephenCleary/AsyncEx e parecer simples como:

await Task.WhenAny(task, source.Token.AsTask());
sonatique
fonte
1
Parece muito complicado ... como toda a implementação de aguardar assinaturas. Eu não acho que essas construções tornem o código fonte mais legível.
Maxim
1
Obrigado, uma observação - o registro do token deve ser descartado posteriormente, a segunda coisa - use ConfigureAwaitcaso contrário, você poderá se machucar nos aplicativos de interface do usuário.
astrowalker
@astrowalker: sim, de fato, o registro do token deve ser não registrado (descartado). Isso pode ser feito dentro do delegado que é passado para Register () chamando dispos no objeto retornado por Register (). Entretanto, desde que "fonte" token é apenas local, neste caso, tudo vai ser limpo de qualquer maneira ...
sonatique
1
Na verdade, tudo o que precisamos é aninhar using.
astrowalker
@astrowalker ;-) sim, você está certo, na verdade. Neste caso, esta é a solução muito mais simples! No entanto, se você deseja retornar o Task.WhenAny diretamente (sem aguardar), precisará de outra coisa. Digo isso porque certa vez encontrei um problema de refatoração como este: antes de usar ... aguarde. Em seguida, removi a espera (e a assíncrona da função), pois era a única, sem perceber que estava quebrando completamente o código. O bug resultante foi difícil de encontrar. Eu, portanto, estou relutante em usar using () junto com async / waitit. Eu sinto o padrão Dispose não vai bem com as coisas assíncronos de qualquer maneira ...
sonatique
15

Um caso que não foi coberto é como lidar com o cancelamento dentro de um método assíncrono. Tomemos, por exemplo, um caso simples em que você precise enviar alguns dados para um serviço, obtê-lo para calcular algo e retornar alguns resultados.

public async Task<Results> ProcessDataAsync(MyData data)
{
    var client = await GetClientAsync();
    await client.UploadDataAsync(data);
    await client.CalculateAsync();
    return await client.GetResultsAsync();
}

Se você deseja dar suporte ao cancelamento, a maneira mais fácil seria passar um token e verificar se ele foi cancelado entre cada chamada de método assíncrona (ou usando ContinueWith). Se as chamadas forem muito longas, você poderá esperar um pouco para cancelar. Eu criei um pequeno método auxiliar para falhar assim que cancelado.

public static class TaskExtensions
{
    public static async Task<T> WaitOrCancel<T>(this Task<T> task, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        await Task.WhenAny(task, token.WhenCanceled());
        token.ThrowIfCancellationRequested();

        return await task;
    }

    public static Task WhenCanceled(this CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
        return tcs.Task;
    }
}

Então, para usá-lo, basta adicionar .WaitOrCancel(token)a qualquer chamada assíncrona:

public async Task<Results> ProcessDataAsync(MyData data, CancellationToken token)
{
    Client client;
    try
    {
        client = await GetClientAsync().WaitOrCancel(token);
        await client.UploadDataAsync(data).WaitOrCancel(token);
        await client.CalculateAsync().WaitOrCancel(token);
        return await client.GetResultsAsync().WaitOrCancel(token);
    }
    catch (OperationCanceledException)
    {
        if (client != null)
            await client.CancelAsync();
        throw;
    }
}

Observe que isso não interromperá a tarefa que você estava esperando e continuará sendo executada. Você precisará usar um mecanismo diferente para interrompê-lo, como a CancelAsyncchamada no exemplo, ou, melhor ainda, passar o mesmo CancellationTokenpara Taskque ele possa lidar com o cancelamento eventualmente. Não é recomendável tentar abortar o thread .

kjbartel
fonte
1
Observe que, enquanto isso cancela a espera pela tarefa, ela não cancela a tarefa real (por exemplo, UploadDataAsyncpode continuar em segundo plano, mas, uma vez concluída, não fará a ligação CalculateAsyncporque essa parte já parou de esperar). Isso pode ou não ser problemático para você, especialmente se você deseja repetir a operação. Passar CancellationTokentodo o caminho é a opção preferida, quando possível.
Miral
1
@Miral que é verdade, no entanto, existem muitos métodos assíncronos que não aceitam tokens de cancelamento. Tomemos, por exemplo, serviços WCF, que quando você gera um cliente com métodos Async, não incluem tokens de cancelamento. De fato, como mostra o exemplo, e como Stephen Cleary também observou, presume-se que tarefas síncronas de longa duração tenham alguma maneira de cancelá-las.
Kjbartel
1
Por isso eu disse "quando possível". Principalmente, eu só queria que essa ressalva fosse mencionada para que as pessoas que encontrassem essa resposta mais tarde não tivessem a impressão errada.
Miral
Obrigado @Miral. Eu atualizei para refletir essa ressalva.
Kjbartel 06/08/19
Infelizmente, isso não funciona com métodos como 'NetworkStream.WriteAsync'.
Zeokat 22/03
6

Eu só quero adicionar à resposta já aceita. Eu estava preso nisso, mas estava seguindo um caminho diferente ao lidar com o evento completo. Em vez de aguardar, adiciono um manipulador completo à tarefa.

Comments.AsAsyncAction().Completed += new AsyncActionCompletedHandler(CommentLoadComplete);

Onde o manipulador de eventos se parece com isso

private void CommentLoadComplete(IAsyncAction sender, AsyncStatus status )
{
    if (status == AsyncStatus.Canceled)
    {
        return;
    }
    CommentsItemsControl.ItemsSource = Comments.Result;
    CommentScrollViewer.ScrollToVerticalOffset(0);
    CommentScrollViewer.Visibility = Visibility.Visible;
    CommentProgressRing.Visibility = Visibility.Collapsed;
}

Com essa rota, todo o tratamento já está feito para você, quando a tarefa é cancelada, apenas aciona o manipulador de eventos e você pode ver se foi cancelado lá.

Smeegs
fonte