¿Cómo cancelar una tarea en espera?


164

Estoy jugando con estas tareas de Windows 8 WinRT, y estoy tratando de cancelar una tarea usando el siguiente método, y funciona hasta cierto punto. Se llama al método CancelNotification, lo que te hace pensar que la tarea se canceló, pero en el fondo la tarea sigue ejecutándose, luego, una vez completada, el estado de la tarea siempre se completa y nunca se cancela. ¿Hay alguna manera de detener completamente la tarea cuando se cancela?

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()
{
}

Acabo de encontrar este artículo que me ayudó a comprender las diversas formas de cancelar.
Uwe Keim

Respuestas:


239

Lea sobre Cancelación (que se introdujo en .NET 4.0 y en gran parte no ha cambiado desde entonces) y el Patrón asincrónico basado en tareas , que proporciona pautas sobre cómo usar CancellationTokencon los asyncmétodos.

Para resumir, pasa un CancellationTokenmétodo a cada método que admite la cancelación, y ese método debe verificarlo periódicamente.

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

2
Wow gran información! Eso funcionó perfectamente, ahora necesito descubrir cómo manejar la excepción en el método asíncrono. ¡Gracias hombre! Leeré las cosas que sugirió.
Carlo

8
No. La mayoría de los métodos síncronos de larga duración tienen alguna forma de cancelarlos, a veces cerrando un recurso subyacente o llamando a otro método. CancellationTokentiene todos los ganchos necesarios para interoperar con sistemas de cancelación personalizados, pero nada puede cancelar un método no cancelable.
Stephen Cleary

1
Ah, ya veo. Entonces, ¿la mejor manera de atrapar la excepción ProcessCancelledException es envolviendo el 'esperar' en un try / catch? A veces obtengo la AggregatedException y no puedo manejar eso.
Carlo

3
Correcto. Te recomiendo que nunca uses Waito Resulten asyncmétodos; siempre debe usarlo await, lo que desenvuelve la excepción correctamente.
Stephen Cleary

11
Solo por curiosidad, ¿hay alguna razón por la que ninguno de los ejemplos use CancellationToken.IsCancellationRequestedy en su lugar sugiera lanzar excepciones?
James M

41

O, para evitar modificaciones slowFunc(por ejemplo, no tiene acceso al código fuente):

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

También puede usar buenos métodos de extensión desde https://github.com/StephenCleary/AsyncEx y hacer que se vea tan simple como:

await Task.WhenAny(task, source.Token.AsTask());

1
Parece muy complicado ... como una implementación completa de espera asíncrona. No creo que tales construcciones hagan que el código fuente sea más legible.
Maxim

1
Gracias, una nota: el token de registro debe desecharse más tarde, lo segundo es usarlo; de lo ConfigureAwaitcontrario, puede lastimarse en las aplicaciones de interfaz de usuario.
astrowalker

@astrowalker: sí, de hecho, el registro del token será mejor que no se registre (se elimine). Esto se puede hacer dentro del delegado que se pasa a Register () llamando a dispose en el objeto que devuelve Register (). Sin embargo, dado que el token "fuente" es solo local en este caso, todo se borrará de todos modos ...
sonatique

1
En realidad, todo lo que se necesita es anidarlo using.
astrowalker

@astrowalker ;-) sí, en realidad tienes razón. ¡En este caso esta es la solución mucho más simple! Sin embargo, si desea devolver Task.WhenAny directamente (sin esperar), entonces necesita algo más. Digo esto porque una vez me topé con un problema de refactorización como este: antes de usar ... espera. Luego eliminé el wait (y el asíncrono en la función) ya que era el único, sin darme cuenta de que estaba rompiendo completamente el código. El error resultante fue difícil de encontrar. Por lo tanto, soy reacio a usar using () junto con async / await. Siento el patrón Desechar no va bien con las cosas asincrónicos de todos modos ...
sonatique

15

Un caso que no se ha cubierto es cómo manejar la cancelación dentro de un método asíncrono. Tomemos, por ejemplo, un caso simple en el que necesita cargar algunos datos en un servicio, hacer que calcule algo y luego devolver algunos resultados.

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

Si desea admitir la cancelación, la forma más fácil sería pasar un token y verificar si se ha cancelado entre cada llamada al método asincrónico (o usando ContinueWith). Si son llamadas de larga duración, es posible que espere un tiempo para cancelar. Creé un pequeño método auxiliar para fallar tan pronto como lo cancele.

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

Entonces, para usarlo, simplemente agregue .WaitOrCancel(token)a cualquier llamada así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;
    }
}

Tenga en cuenta que esto no detendrá la Tarea que estaba esperando y continuará ejecutándose. Tendrá que usar un mecanismo diferente para detenerlo, como la CancelAsyncllamada en el ejemplo, o mejor aún, pasar el mismo CancellationTokenal Taskpara que pueda manejar la cancelación eventualmente. Intentar abortar el hilo no es recomendable .


1
Tenga en cuenta que si bien esto cancela la espera de la tarea, no cancela la tarea real (por ejemplo, UploadDataAsyncpuede continuar en segundo plano, pero una vez que se complete, no realizará la llamada CalculateAsyncporque esa parte ya dejó de esperar). Esto puede o no ser problemático para usted, especialmente si desea volver a intentar la operación. Pasar CancellationTokentodo el camino hacia abajo es la opción preferida, cuando sea posible.
Miral

1
@Miral es cierto, sin embargo, hay muchos métodos asíncronos que no aceptan tokens de cancelación. Tomemos, por ejemplo, los servicios WCF, que cuando genere un cliente con métodos asíncronos no incluirá tokens de cancelación. De hecho, como muestra el ejemplo, y como también señaló Stephen Cleary, se supone que las tareas sincrónicas de larga duración tienen alguna forma de cancelarlas.
kjbartel

1
Por eso dije "cuando sea posible". Principalmente, solo quería que se mencionara esta advertencia para que las personas que encuentren esta respuesta más tarde no tengan la impresión equivocada.
Miral

@Miral Gracias. He actualizado para reflejar esta advertencia.
kjbartel

Lamentablemente, esto no funciona con métodos como 'NetworkStream.WriteAsync'.
Zeokat

6

Solo quiero agregar a la respuesta ya aceptada. Estaba atrapado en esto, pero estaba yendo por una ruta diferente en el manejo del evento completo. En lugar de ejecutar aguardar, agrego un controlador completado a la tarea.

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

Donde el controlador de eventos se ve así

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

Con esta ruta, todo el manejo ya está hecho para usted, cuando la tarea se cancela, solo activa el controlador de eventos y puede ver si se canceló allí.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.