Token di annullamento nel costruttore attività: perché?


223

Alcuni System.Threading.Tasks.Taskcostruttori prendono a CancellationTokencome parametro:

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

Ciò che mi sorprende di questo è che non c'è modo dall'interno del corpo del metodo di ottenere effettivamente il token passato (ad esempio, niente di simile Task.CurrentTask.CancellationToken). Il token deve essere fornito tramite qualche altro meccanismo, come l'oggetto stato o catturato in un lambda.

Quindi, a cosa serve fornire il token di annullamento nel costruttore?

Risposte:


254

Passare a CancellationTokennel Taskcostruttore lo associa all'attività.

Citando la risposta di Stephen Toub da MSDN :

Questo ha due vantaggi principali:

  1. Se il token ha richiesto la cancellazione prima Taskdell'inizio dell'esecuzione, Tasknon verrà eseguito. Invece di passare a Running, passerà immediatamente a Canceled. Questo evita i costi di esecuzione dell'attività se venisse semplicemente annullata durante l'esecuzione.
  2. Se il corpo dell'attività sta anche monitorando il token di annullamento e genera un OperationCanceledExceptiontoken contenente tale (che è ciò che ThrowIfCancellationRequestedfa), quindi quando l'attività lo vede OperationCanceledException, controlla se il OperationCanceledExceptiontoken corrisponde al token dell'attività. In tal caso, tale eccezione viene considerata come un riconoscimento della cancellazione cooperativa e delle Tasktransizioni verso lo Canceled stato (piuttosto che verso lo Faultedstato).

2
Il TPL è così ben pensato.
Colonnello Panic,

1
Suppongo che il beneficio 1 si applichi in modo simile al passaggio di un token di annullamento a Parallel.ForoParallel.ForEach
Colonnello Panic,

27

Il costruttore utilizza il token per la gestione delle cancellazioni internamente. Se il tuo codice desidera accedere al token, sei responsabile del passaggio a te stesso. Consiglio vivamente di leggere il libro Parallel Programming con Microsoft .NET su CodePlex .

Esempio di utilizzo di CTS dal 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
e cosa succede se non si passa il token come parametro? Sembra che il comportamento sarà lo stesso, senza scopo.
sergtk,

2
@sergdev: si passa il token per registrarlo con l'attività e lo scheduler. Non passarlo e usarlo sarebbe un comportamento indefinito.
user7116

3
@sergdev: dopo il test: myTask.IsCanceled e myTask.Status non sono gli stessi quando non si passa il token come parametro. Lo stato verrà fallito anziché annullato. Tuttavia l'eccezione è la stessa: è un'operazione OperationCanceledException in entrambi i casi.
Olivier de Rivoyre,

2
Cosa succede se non chiamo token.ThrowIfCancellationRequested();? Nel mio test, il comportamento è lo stesso. Qualche idea?
machinarium,

1
@CobaltBlue: no when cts.Cancel() is called the Task is going to get canceled and end, no matter what you do. Se l'attività viene annullata prima di essere avviata, viene annullata . Se il corpo dell'attività non verifica mai alcun token, verrà eseguito fino al completamento, con conseguente stato RanToCompletion . Se il corpo lancia un OperationCancelledException, ad esempio da ThrowIfCancellationRequested, Task verificherà se la cancellazione di quell'eccezione è uguale a quella associata all'attività. In tal caso, l'attività viene annullata . In caso contrario, è difettoso .
Wolfzoon,

7

La cancellazione non è un caso semplice come molti potrebbero pensare. Alcune delle sottigliezze sono spiegate in questo post del blog su msdn:

Per esempio:

In alcune situazioni nelle estensioni parallele e in altri sistemi, è necessario riattivare un metodo bloccato per motivi che non sono dovuti alla cancellazione esplicita da parte di un utente. Ad esempio, se un thread è bloccato a blockingCollection.Take()causa della raccolta vuota e un altro thread successivamente chiama blockingCollection.CompleteAdding(), quindi la prima chiamata dovrebbe riattivarsi e lanciare un InvalidOperationExceptionper rappresentare un utilizzo errato.

Annullamento in estensioni parallele


4

Ecco un esempio che dimostra i due punti nella risposta di 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;
            }
        }
    }
}

Produzione:

*********************************************************************
* 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!!!
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.