Token de cancelación en el constructor de tareas: ¿por qué?


223

Ciertos System.Threading.Tasks.Taskconstructores toman a CancellationTokencomo parámetro:

CancellationTokenSource source = new CancellationTokenSource();
Task t = new Task (/* method */, source.Token);

Lo que me desconcierta acerca de esto es que no hay forma desde el interior del cuerpo del método para llegar realmente al token pasado (por ejemplo, nada parecido Task.CurrentTask.CancellationToken). El token debe proporcionarse a través de algún otro mecanismo, como el objeto de estado o capturado en una lambda.

Entonces, ¿para qué sirve proporcionar el token de cancelación en el constructor?

Respuestas:


254

Pasar un CancellationTokenal Taskconstructor lo asocia con la tarea.

Citando la respuesta de Stephen Toub de MSDN :

Esto tiene dos beneficios principales:

  1. Si el token tiene una solicitud de cancelación solicitada antes de Taskcomenzar a ejecutarse, Taskno se ejecutará. En lugar de hacer la transición a Running, inmediatamente hará la transición a Canceled. Esto evita los costos de ejecutar la tarea si de todos modos se cancelara mientras se ejecuta.
  2. Si el cuerpo de la tarea también está monitoreando el token de cancelación y lanza un OperationCanceledExceptiontoken que contiene ese token (que es lo que ThrowIfCancellationRequestedhace), entonces cuando la tarea lo ve OperationCanceledException, verifica si el OperationCanceledExceptiontoken de la partida coincide con el token de la Tarea. Si lo hace, esa excepción se considera como un reconocimiento de cancelación cooperativa y las Tasktransiciones al Canceled estado (en lugar del Faultedestado).

2
El TPL está muy bien pensado.
Coronel Panic

1
Supongo que el beneficio 1 se aplica de manera similar a pasar un token de cancelación a Parallel.ForoParallel.ForEach
Coronel Panic

27

El constructor usa el token para el manejo de cancelación internamente. Si su código desea acceder al token, usted es responsable de pasárselo. Recomiendo leer el libro Programación en paralelo con Microsoft .NET en CodePlex .

Ejemplo de uso de CTS del libro:

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task myTask = Task.Factory.StartNew(() =>
{
    for (...)
    {
        token.ThrowIfCancellationRequested();

        // Body of for loop.
    }
}, token);

// ... elsewhere ...
cts.Cancel();

3
¿Y qué sucede si no pasa el token como parámetro? Parece que el comportamiento será el mismo, sin ningún propósito.
sergtk

2
@sergdev: pasa el token para registrarlo con la tarea y el planificador. No pasarlo y usarlo sería un comportamiento indefinido.
user7116

3
@sergdev: después de probar: myTask.IsCanceled y myTask.Status no son iguales cuando no pasa el token como parámetro. El estado fallará en lugar de cancelarse. Sin embargo, la excepción es la misma: es una OperationCanceledException en ambos casos.
Olivier de Rivoyre

2
¿Qué pasa si no llamo token.ThrowIfCancellationRequested();? En mi prueba, el comportamiento es el mismo. ¿Algunas ideas?
machinarium

1
@ CobaltBlue: no when cts.Cancel() is called the Task is going to get canceled and end, no matter what you do. Si la tarea se cancela antes de que haya comenzado, se cancela . Si el cuerpo de la Tarea simplemente nunca verifica ningún token, se ejecutará hasta su finalización, dando como resultado un estado RanToCompletion . Si el cuerpo arroja un OperationCancelledException, p. Ej., Por ThrowIfCancellationRequested, entonces la Tarea verificará si el CancellationToken de esa Excepción es el mismo que el asociado con la Tarea. Si es así, la tarea se cancela . Si no, es culpable .
Wolfzoon

7

La cancelación no es un caso simple como muchos podrían pensar. Algunas de las sutilezas se explican en esta publicación de blog en msdn:

Por ejemplo:

En ciertas situaciones en Parallel Extensions y en otros sistemas, es necesario activar un método bloqueado por razones que no se deben a una cancelación explícita por parte de un usuario. Por ejemplo, si un subproceso está bloqueado blockingCollection.Take()debido a que la colección está vacía y otro subproceso llama posteriormente blockingCollection.CompleteAdding(), entonces la primera llamada debe despertarse y lanzar un símbolo InvalidOperationExceptionpara representar un uso incorrecto.

Cancelación en extensiones paralelas


4

Aquí hay un ejemplo que demuestra los dos puntos en la respuesta de Max Galkin :

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(true);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(true);
        Console.WriteLine();

        Console.WriteLine();
        Console.WriteLine("Test Done!!!");
        Console.ReadKey();
    }

    static void StartCanceledTaskTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, false), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, false));
        }

        Console.WriteLine("Canceling task");
        tokenSource.Cancel();

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void ThrowIfCancellationRequestedTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, true), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, true));
        }

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            Thread.Sleep(100);

            Console.WriteLine("Canceling task");
            tokenSource.Cancel();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void TaskWork(CancellationToken token, bool throwException)
    {
        int loopCount = 0;

        while (true)
        {
            loopCount++;
            Console.WriteLine("Task: loop count {0}", loopCount);

            token.WaitHandle.WaitOne(50);
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Task: cancellation requested");
                if (throwException)
                {
                    token.ThrowIfCancellationRequested();
                }

                break;
            }
        }
    }
}

Salida:

*********************************************************************
* Start canceled task, don't pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Task: loop count 1
Task: cancellation requested
Task.Status: RanToCompletion

*********************************************************************
* Start canceled task, pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Exception: Start may not be called on a task that has completed.
Task.Status: Canceled

*********************************************************************
* Throw if cancellation requested, don't pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: The operation was canceled.
Task.Status: Faulted

*********************************************************************
* Throw if cancellation requested, pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: A task was canceled.
Task.Status: Canceled


Test Done!!!
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.