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?

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).

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();

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


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!!!
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.