Jeton d'annulation dans le constructeur de tâches: pourquoi?

223

Certains System.Threading.Tasks.Taskconstructeurs prennent un CancellationTokencomme paramètre:

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

Ce qui me déroute à ce sujet, c'est qu'il n'y a aucun moyen de l' intérieur du corps de la méthode pour réellement accéder au jeton transmis (par exemple, rien de tel Task.CurrentTask.CancellationToken). Le jeton doit être fourni via un autre mécanisme, tel que l'objet d'état ou capturé dans un lambda.

Alors, à quoi sert le jeton d'annulation dans le constructeur?

Colin
la source

Réponses:

254

Passer un CancellationTokendans le Taskconstructeur l'associe à la tâche.

Citant la réponse de Stephen Toub de MSDN :

Cela présente deux avantages principaux:

  1. Si l'annulation du jeton a été demandée avant le Taskdébut de l'exécution, le Taskne s'exécutera pas. Plutôt que de passer à Running, il passera immédiatement à Canceled. Cela évite les coûts d'exécution de la tâche si elle venait à être annulée lors de l'exécution de toute façon.
  2. Si le corps de la tâche surveille également le jeton d'annulation et lance un OperationCanceledExceptioncontenant ce jeton (ce qui est le ThrowIfCancellationRequestedcas), alors lorsque la tâche voit cela OperationCanceledException, il vérifie si le OperationCanceledExceptionjeton de correspond au jeton de la tâche. Si tel est le cas, cette exception est considérée comme une reconnaissance de l'annulation coopérative et des Tasktransitions vers l' Canceled État (plutôt que l' FaultedÉtat).
Max Galkin
la source
2
Le TPL est si bien pensé.
Colonel Panic
1
Je suppose que l'avantage 1 s'applique de la même manière au passage d'un jeton d'annulation à Parallel.ForouParallel.ForEach
Colonel Panic
27

Le constructeur utilise le jeton pour la gestion des annulations en interne. Si votre code souhaite accéder au jeton, vous êtes responsable de vous le transmettre. Je recommande fortement de lire le livre sur la programmation parallèle avec Microsoft .NET sur CodePlex .

Exemple d'utilisation de CTS du livre:

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
la source
3
et que se passe-t-il si vous ne passez pas le jeton en paramètre? On dirait que le comportement sera le même, sans but.
sergtk
2
@sergdev: vous passez le jeton pour l'enregistrer auprès de la tâche et du planificateur. Ne pas le passer et l'utiliser ne serait pas un comportement défini.
user7116
3
@sergdev: après le test: myTask.IsCanceled et myTask.Status ne sont pas identiques lorsque vous ne passez pas le jeton en paramètre. Le statut échouera au lieu d'être annulé. Néanmoins, l'exception est la même: c'est une OperationCanceledException dans les deux cas.
Olivier de Rivoyre
2
Et si je n'appelle pas token.ThrowIfCancellationRequested();? Dans mon test, le comportement est le même. Des idées?
machinarium du
1
@CobaltBlue: non when cts.Cancel() is called the Task is going to get canceled and end, no matter what you do. Si la tâche est annulée avant d'avoir démarré, elle est annulée . Si le corps de la tâche ne vérifie simplement aucun jeton, il s'exécutera jusqu'à son terme, ce qui entraînera un état RanToCompletion . Si le corps lance un OperationCancelledException, par exemple par ThrowIfCancellationRequested, alors la tâche vérifiera si le jeton d'annulation de cette exception est le même que celui associé à la tâche. Si c'est le cas, la tâche est annulée . Sinon, c'est défectueux .
Wolfzoon
7

L'annulation n'est pas un cas simple comme beaucoup pourraient le penser. Certaines des subtilités sont expliquées dans cet article de blog sur msdn:

Par exemple:

Dans certaines situations dans Parallel Extensions et dans d'autres systèmes, il est nécessaire de réveiller une méthode bloquée pour des raisons qui ne sont pas dues à une annulation explicite par un utilisateur. Par exemple, si un thread est bloqué en blockingCollection.Take()raison de la collection vide et qu'un autre thread appelle par la suite blockingCollection.CompleteAdding(), le premier appel doit se réveiller et lancer un InvalidOperationExceptionpour représenter une utilisation incorrecte.

Annulation dans les extensions parallèles

x0n
la source
4

Voici un exemple qui illustre les deux points de la réponse 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;
            }
        }
    }
}

Production:

*********************************************************************
* 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
la source