Procedura: annullare un'attività e i relativi figli

In questo esempio viene illustrato come eseguire le attività seguenti:

  1. Creare e avviare un'attività annullabile.
  2. Passare un token di annullamento al delegato dell'utente e, facoltativamente, all'istanza dell'attività.
  3. Rilevare e rispondere alla richiesta di annullamento nel delegato dell'utente.
  4. Facoltativamente, indicare nel thread chiamante che l'attività è stata annullata.

Il thread chiamante non forza la fine dell'attività, ma segnala solo che è richiesto l'annullamento. Se l'attività è già in esecuzione, è responsabilità del delegato dell'utente rilevare la richiesta e rispondervi nel modo appropriato. Se l'annullamento viene richiesto prima dell'esecuzione dell'attività, il delegato dell'utente non viene mai eseguito e l'oggetto attività passa allo stato annullato.

Esempio

Questo esempio mostra come terminare un'attività Task e i relativi elementi figlio in risposta a una richiesta di annullamento. Viene inoltre mostrato che quando un delegato dell'utente viene terminato generando un oggetto TaskCanceledException, tramite il thread chiamante è possibile utilizzare facoltativamente il metodo Wait o il metodo WaitAll per attendere il completamento delle attività. In questo caso, è necessario utilizzare un blocco try/catch per gestire le eccezioni nel thread chiamante.

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
    public static async Task Main()
    {
        // Cancellation token source for cancellation. Make sure to dispose after use (which is done here through the using expression).
        using var tokenSource = new CancellationTokenSource();

        // The cancellation token will be used to communicate cancellation to tasks
        var token = tokenSource.Token;

        Console.WriteLine("Main: Press any key to begin tasks...");
        Console.ReadKey(true);
        Console.WriteLine("Main: To terminate the example, press 'c' to cancel and exit...");
        Console.WriteLine();

        // Store references to the tasks so that we can wait on them and
        // observe their status after cancellation.
        var tasks = new ConcurrentBag<Task>();

        // Pass the token to the user delegate so it can cancel during execution,
        // and also to the task so it can cancel before execution starts.
        var cancellableTask = Task.Run(() => {
            DoSomeWork(token);
            Console.WriteLine("Cancellable: Task {0} ran to completion", Task.CurrentId);
        }, token);
        Console.WriteLine("Main: Cancellable Task {0} created", cancellableTask.Id);
        tasks.Add(cancellableTask);

        var parentTask = Task.Run(() =>
        {
            for (int i = 0; i <= 7; i++)
            {
                // If cancellation was requested we don't need to start any more
                // child tasks (that would immediately cancel) => break out of loop
                if (token.IsCancellationRequested) break;

                // For each child task, pass the same token
                // to each user delegate and to Task.Run.
                var childTask = Task.Run(() => {
                    DoSomeWork(token);
                    Console.WriteLine("Child: Task {0} ran to completion", Task.CurrentId);
                }, token);
                Console.WriteLine("Parent: Task {0} created", childTask.Id);
                tasks.Add(childTask);

                DoSomeWork(token, maxIterations: 1);
            }

            Console.WriteLine("Parent: Task {0} ran to completion", Task.CurrentId);
        }, token);
        Console.WriteLine("Main: Parent Task {0} created", parentTask.Id);
        tasks.Add(parentTask);

        // Request cancellation from the UI thread.
        char ch = Console.ReadKey().KeyChar;
        if (ch == 'c' || ch == 'C')
        {
            tokenSource.Cancel();
            Console.WriteLine("\nMain: Task cancellation requested.");

            // Optional: Observe the change in the Status property on the task.
            // It is not necessary to wait on tasks that have canceled. However,
            // if you do wait, you must enclose the call in a try-catch block to
            // catch the OperationCanceledExceptions that are thrown. If you do
            // not wait, no exception is thrown if the token that was passed to the
            // Task.Run method is the same token that requested the cancellation.
        }

        try
        {
            // Wait for all tasks before disposing the cancellation token source
            await Task.WhenAll(tasks);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine($"\nMain: {nameof(OperationCanceledException)} thrown\n");
        }

        // Display status of all tasks.
        foreach (var task in tasks)
        {
            Console.WriteLine("Main: Task {0} status is now {1}", task.Id, task.Status);
        }
    }

    static void DoSomeWork(CancellationToken ct, int maxIterations = 10)
    {
        // Was cancellation already requested?
        if (ct.IsCancellationRequested)
        {
            Console.WriteLine("Task {0} was cancelled before it got started.", Task.CurrentId);
            ct.ThrowIfCancellationRequested();
        }

        // NOTE!!! A "TaskCanceledException was unhandled
        // by user code" error will be raised here if "Just My Code"
        // is enabled on your computer. On Express editions JMC is
        // enabled and cannot be disabled. The exception is benign.
        // Just press F5 to continue executing your code.
        for (int i = 0; i <= maxIterations; i++)
        {
            // Do a bit of work. Not too much.
            var sw = new SpinWait();
            for (int j = 0; j <= 100; j++)
                sw.SpinOnce();

            if (ct.IsCancellationRequested)
            {
                Console.WriteLine("Task {0} work cancelled", Task.CurrentId);
                ct.ThrowIfCancellationRequested();
            }
        }
    }
}
// The example displays output like the following:
//    Main: Press any key to begin tasks...
//    Main: To terminate the example, press 'c' to cancel and exit...
//
//    Main: Cancellable Task 13 created
//    Main: Parent Task 14 created
//    Parent: Task 15 created
//    Parent: Task 16 created
//    Parent: Task 17 created
//    Parent: Task 18 created
//    Parent: Task 19 created
//    Parent: Task 20 created
//    Cancellable: Task 13 ran to completion
//    Child: Task 15 ran to completion
//    Parent: Task 21 created
//    Child: Task 16 ran to completion
//    Parent: Task 22 created
//    Child: Task 17 ran to completion
//    c
//    Main: Task cancellation requested.
//    Task 20 work cancelled
//    Task 21 work cancelled
//    Task 22 work cancelled
//    Task 18 work cancelled
//    Task 14 work cancelled
//    Task 19 work cancelled
//
//    Main: OperationCanceledException thrown
//
//    Main: Task 22 status is now Canceled
//    Main: Task 21 status is now Canceled
//    Main: Task 20 status is now Canceled
//    Main: Task 19 status is now Canceled
//    Main: Task 18 status is now Canceled
//    Main: Task 17 status is now RanToCompletion
//    Main: Task 16 status is now RanToCompletion
//    Main: Task 15 status is now RanToCompletion
//    Main: Task 14 status is now Canceled
//    Main: Task 13 status is now RanToCompletion
Imports System.Collections.Concurrent
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Sub Main()
        Dim tokenSource As New CancellationTokenSource()
        Dim token As CancellationToken = tokenSource.Token

        ' Store references to the tasks so that we can wait on them and
        ' observe their status after cancellation.
        Dim t As Task
        Dim tasks As New ConcurrentBag(Of Task)()

        Console.WriteLine("Press any key to begin tasks...")
        Console.ReadKey(True)
        Console.WriteLine("To terminate the example, press 'c' to cancel and exit...")
        Console.WriteLine()

        ' Request cancellation of a single task when the token source is canceled.
        ' Pass the token to the user delegate, and also to the task so it can
        ' handle the exception correctly.
        t = Task.Factory.StartNew(Sub() DoSomeWork(token), token)
        Console.WriteLine("Task {0} executing", t.Id)
        tasks.Add(t)

        ' Request cancellation of a task and its children. Note the token is passed
        ' to (1) the user delegate and (2) as the second argument to StartNew, so
        ' that the task instance can correctly handle the OperationCanceledException.
        t = Task.Factory.StartNew(Sub()
                                      ' Create some cancelable child tasks.
                                      Dim tc As Task
                                      For i As Integer = 3 To 10
                                          ' For each child task, pass the same token
                                          ' to each user delegate and to StartNew.
                                          tc = Task.Factory.StartNew(Sub(iteration) DoSomeWork(token), i, token)
                                          Console.WriteLine("Task {0} executing", tc.Id)
                                          tasks.Add(tc)
                                          ' Pass the same token again to do work on the parent task.
                                          ' All will be signaled by the call to tokenSource.Cancel below.
                                          DoSomeWork(token)
                                      Next
                                  End Sub,
                                  token)

        Console.WriteLine("Task {0} executing", t.Id)
        tasks.Add(t)

        ' Request cancellation from the UI thread.
        Dim ch As Char = Console.ReadKey().KeyChar
        If ch = "c"c Or ch = "C"c Then
            tokenSource.Cancel()
            Console.WriteLine(vbCrLf + "Task cancellation requested.")

            ' Optional: Observe the change in the Status property on the task.
            ' It is not necessary to wait on tasks that have canceled. However,
            ' if you do wait, you must enclose the call in a try-catch block to
            ' catch the OperationCanceledExceptions that are thrown. If you do
            ' not wait, no exception is thrown if the token that was passed to the
            ' StartNew method is the same token that requested the cancellation.
        End If

        Try
            Task.WaitAll(tasks.ToArray())
        Catch e As AggregateException
            Console.WriteLine()
            Console.WriteLine("AggregateException thrown with the following inner exceptions:")
            ' Display information about each exception.
            For Each v In e.InnerExceptions
                If TypeOf v Is OperationCanceledException Then
                    Console.WriteLine("   The operation was canceled.")
                Else
                    Console.WriteLine("   Exception: {0}", v.GetType().Name)
                End If
            Next
            Console.WriteLine()
        Finally
            tokenSource.Dispose()
        End Try

        ' Display status of all tasks.
        For Each t In tasks
            Console.WriteLine("Task {0} status is now {1}", t.Id, t.Status)
        Next
    End Sub

    Sub DoSomeWork(ByVal ct As CancellationToken)
        ' Was cancellation already requested?
        If ct.IsCancellationRequested = True Then
            Console.WriteLine("Task {0} was cancelled before it got started.",
                              Task.CurrentId)
            ct.ThrowIfCancellationRequested()
        End If

        Dim maxIterations As Integer = 100

        ' NOTE!!! A "TaskCanceledException was unhandled
        ' by user code" error will be raised here if "Just My Code"
        ' is enabled on your computer. On Express editions JMC is
        ' enabled and cannot be disabled. The exception is benign.
        ' Just press F5 to continue executing your code.
        For i As Integer = 0 To maxIterations
            ' Do a bit of work. Not too much.
            Dim sw As New SpinWait()
            For j As Integer = 0 To 100
                sw.SpinOnce()
            Next
            If ct.IsCancellationRequested Then
                Console.WriteLine("Task {0} cancelled", Task.CurrentId)
                ct.ThrowIfCancellationRequested()
            End If
        Next
    End Sub
End Module
' The example displays output like the following:
'    Press any key to begin tasks...
'    To terminate the example, press 'c' to cancel and exit...
'
'    Task 1 executing
'    Task 2 executing
'    Task 3 executing
'    Task 4 executing
'    Task 5 executing
'    Task 6 executing
'    Task 7 executing
'    Task 8 executing
'    c
'    Task cancellation requested.
'    Task 2 cancelled
'    Task 7 cancelled
'
'    AggregateException thrown with the following inner exceptions:
'       TaskCanceledException: Task 2
'       TaskCanceledException: Task 8
'       TaskCanceledException: Task 7
'
'    Task 2 status is now Canceled
'    Task 1 status is now RanToCompletion
'    Task 8 status is now Canceled
'    Task 7 status is now Canceled
'    Task 6 status is now RanToCompletion
'    Task 5 status is now RanToCompletion
'    Task 4 status is now RanToCompletion
'    Task 3 status is now RanToCompletion

La classe System.Threading.Tasks.Task è completamente integrata con il modello di annullamento basato sui tipi System.Threading.CancellationTokenSource e System.Threading.CancellationToken. Per altre informazioni, vedere Annullamento in thread gestiti e Annullamento delle attività.

Vedi anche