Token anulowania w Konstruktorze zadań: dlaczego?

223

Niektóre System.Threading.Tasks.Taskkonstruktory przyjmują CancellationTokenjako parametr:

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

Zaskakuje mnie to, że z wnętrza metody nie ma sposobu, aby faktycznie dostać się do przekazanego tokena (np. Nic podobnego Task.CurrentTask.CancellationToken). Token musi być dostarczony przez inny mechanizm, taki jak obiekt stanu lub przechwycony w lambda.

W jakim celu służy token anulowania w konstruktorze?

Colin
źródło

Odpowiedzi:

254

Przekazywanie a CancellationTokendo Taskkonstruktora wiąże to z zadaniem.

Cytując odpowiedź Stephena Touba z MSDN :

Ma to dwie podstawowe zalety:

  1. Jeśli przed uruchomieniem tokena zażądano anulowania Task, Tasknie zostanie on wykonany. Zamiast przejść do Running, natychmiast przejdzie do Canceled. Pozwala to uniknąć kosztów uruchomienia zadania, jeśli i tak zostanie ono anulowane podczas uruchamiania.
  2. Jeśli ciało zadania monitoruje również token anulowania i rzuca OperationCanceledExceptionzawierający go token (co się ThrowIfCancellationRequesteddzieje), to kiedy zadanie to zobaczy OperationCanceledException, sprawdza, czy OperationCanceledExceptiontoken pasuje do tokena zadania. Jeśli tak, wyjątek ten jest postrzegany jako potwierdzenie anulowania spółdzielczego i Taskprzejścia do Canceled stanu (a nie Faultedstanu).
Max Galkin
źródło
2
TPL jest tak dobrze przemyślana.
Pułkownik Panic
1
Zakładam, że korzyść 1 stosuje się podobnie do przekazania tokena anulowania do Parallel.ForlubParallel.ForEach
Pułkownik Panic
27

Konstruktor używa tokena do wewnętrznego anulowania. Jeśli Twój kod chce uzyskać dostęp do tokena, jesteś odpowiedzialny za przekazanie go tobie. Gorąco polecam przeczytanie książki o programowaniu równoległym z Microsoft .NET w CodePlex .

Przykład użycia CTS z książki:

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

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

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

// ... elsewhere ...
cts.Cancel();
użytkownik7116
źródło
3
a co się stanie, jeśli nie podasz tokena jako parametru? Wygląda na to, że zachowanie będzie takie samo, bez celu.
sergtk,
2
@sergdev: przekazujesz token, aby zarejestrować go w zadaniu i harmonogramie. Nieprzekazanie go i użycie go byłoby niezdefiniowanym zachowaniem.
user7116
3
@sergdev: po przetestowaniu: myTask.IsCanceled i myTask.Status nie są takie same, jeśli nie podasz tokena jako parametru. Status zostanie odrzucony zamiast anulowany. Niemniej jednak wyjątek jest taki sam: w obu przypadkach jest to wyjątek OperationCanceledException.
Olivier de Rivoyre,
2
Co jeśli nie zadzwonię token.ThrowIfCancellationRequested();? W moim teście zachowanie jest takie samo. Jakieś pomysły?
machinarium
1
@CobaltBlue: nie when cts.Cancel() is called the Task is going to get canceled and end, no matter what you do. Jeśli zadanie zostanie anulowane przed jego uruchomieniem, zostanie anulowane . Jeśli treść zadania nigdy nie sprawdza żadnego tokena, uruchomi się on do końca, co spowoduje status RanToCompletion . Jeśli ciało wyrzuci OperationCancelledExceptionnp. Przez ThrowIfCancellationRequested, wówczas Task sprawdzi, czy token Cancellation tego wyjątku jest taki sam jak ten powiązany z Zadaniem. Jeśli tak, zadanie jest anulowane . Jeśli nie, to nic zarzucić .
Wolfzoon,
7

Anulowanie nie jest prostym przypadkiem, jak wielu może pomyśleć. Niektóre subtelności są wyjaśnione w tym poście na blogu na msdn:

Na przykład:

W niektórych sytuacjach w rozszerzeniach równoległych i innych systemach konieczne jest wznowienie zablokowanej metody z powodów, które nie są spowodowane wyraźnym anulowaniem przez użytkownika. Na przykład, jeśli jeden wątek jest zablokowany z blockingCollection.Take()powodu pustej kolekcji, a następnie kolejny wątek wywołuje blockingCollection.CompleteAdding(), wówczas pierwsze połączenie powinno się obudzić i rzucić a, InvalidOperationExceptionaby reprezentować nieprawidłowe użycie.

Anulowanie w rozszerzeniach równoległych

x0n
źródło
4

Oto przykład, który pokazuje dwa punkty w odpowiedzi przez Maxa 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;
            }
        }
    }
}

Wynik:

*********************************************************************
* 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
źródło