Token de cancelamento no construtor Task: por quê?

223

Certos System.Threading.Tasks.Taskconstrutores aceitam a CancellationTokencomo parâmetro:

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

O que me deixa desconcertado é que não há como, dentro do corpo do método, obter o token passado (por exemplo, nada parecido Task.CurrentTask.CancellationToken). O token deve ser fornecido através de algum outro mecanismo, como o objeto state ou capturado em uma lambda.

Então, qual é a finalidade do fornecimento do token de cancelamento no construtor?

Colin
fonte

Respostas:

254

Passar um CancellationTokenpara o Taskconstrutor o associa à tarefa.

Citando a resposta de Stephen Toub do MSDN :

Isso tem dois benefícios principais:

  1. Se o token tiver solicitado o cancelamento antes do Taskinício da execução, Taskele não será executado. Em vez de fazer a transição para Running, ele fará a transição imediatamente para Canceled. Isso evita os custos de execução da tarefa, caso ela apenas seja cancelada durante a execução.
  2. Se o corpo da tarefa também estiver monitorando o token de cancelamento e lançar um OperationCanceledExceptioncontendo esse token (que é o que ThrowIfCancellationRequestedfunciona), quando a tarefa vir isso OperationCanceledException, ele verificará se o OperationCanceledExceptiontoken da correspondência corresponde ao token da tarefa. Nesse caso, essa exceção é vista como um reconhecimento do cancelamento cooperativo e das Tasktransições para o Canceled estado (e não para o Faultedestado).
Max Galkin
fonte
2
O TPL é tão bem pensado.
Coronel Panic
1
Presumo que o benefício 1 se aplique de maneira semelhante à passagem de um token de cancelamento para Parallel.ForouParallel.ForEach
Coronel Panic
27

O construtor usa o token para tratamento de cancelamento internamente. Se o seu código deseja acessar o token, você é responsável por transmiti-lo a si mesmo. Eu recomendo a leitura do livro Programação Paralela com Microsoft .NET no CodePlex .

Exemplo de uso do CTS do livro:

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

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

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

// ... elsewhere ...
cts.Cancel();
user7116
fonte
3
e o que acontece se você não passar o token como parâmetro? Parece que o comportamento será o mesmo, sem propósito.
sergtk
2
@ergdev: você passa o token para registrá-lo na tarefa e no agendador. Não passar e usá-lo seria um comportamento indefinido.
precisa saber é o seguinte
3
@sergdev: após o teste: myTask.IsCanceled e myTask.Status não são os mesmos quando você não passa o token como parâmetro. O status falhará em vez de ser cancelado. No entanto, a exceção é a mesma: é uma OperationCanceledException nos dois casos.
Olivier de Rivoyre
2
E se eu não ligar token.ThrowIfCancellationRequested();? No meu teste, o comportamento é o mesmo. Alguma ideia?
machinarium
1
@CobaltBlue: when cts.Cancel() is called the Task is going to get canceled and end, no matter what you donão. Se a tarefa for cancelada antes de ser iniciada, será cancelada . Se o corpo da tarefa simplesmente nunca verificar nenhum token, ele será executado até a conclusão, resultando em um status RanToCompletion . Se o corpo lançar um OperationCancelledException, por exemplo, by ThrowIfCancellationRequested, a Task verificará se o CancelamentoToken da exceção é igual ao associado à tarefa. Se for, a tarefa é cancelada . Caso contrário, está com falha .
Wolfzoon 03/10/19
7

O cancelamento não é um caso simples, como muitos podem pensar. Algumas das sutilezas são explicadas nesta postagem do blog no msdn:

Por exemplo:

Em certas situações nas extensões paralelas e em outros sistemas, é necessário ativar um método bloqueado por razões que não são devidas ao cancelamento explícito por um usuário. Por exemplo, se um encadeamento estiver bloqueado blockingCollection.Take()devido ao fato de a coleção estar vazia e outro encadear posteriormente blockingCollection.CompleteAdding(), a primeira chamada deve ser ativada e acionar um InvalidOperationExceptionpara representar um uso incorreto.

Cancelamento em extensões paralelas

x0n
fonte
4

Aqui está um exemplo que demonstra os dois pontos na resposta 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;
            }
        }
    }
}

Resultado:

*********************************************************************
* 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!!!
Eliahu Aaron
fonte