Token pembatalan di konstruktor tugas: mengapa?

223

System.Threading.Tasks.TaskKonstruktor tertentu menggunakan CancellationTokensebagai parameter:

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

Apa yang membuat saya bingung tentang ini adalah bahwa tidak ada cara dari dalam tubuh metode untuk benar-benar mendapatkan token yang disahkan (misalnya, tidak seperti Task.CurrentTask.CancellationToken). Token harus disediakan melalui beberapa mekanisme lain, seperti objek negara atau ditangkap dalam lambda.

Jadi tujuan apa yang menyediakan token pembatalan dalam melayani konstruktor?

Colin
sumber

Jawaban:

254

Melewati a CancellationTokenke Taskkonstruktor mengaitkannya dengan tugas.

Mengutip jawaban Stephen Toub dari MSDN :

Ini memiliki dua manfaat utama:

  1. Jika token memiliki pembatalan yang diminta sebelum Taskmulai untuk mengeksekusi, token Tasktidak akan mengeksekusi. Daripada beralih ke Running, itu akan segera beralih ke Canceled. Ini menghindari biaya menjalankan tugas jika itu hanya akan dibatalkan saat menjalankan saja.
  2. Jika badan tugas juga memantau token pembatalan dan melempar yang OperationCanceledExceptionberisi token itu (yang adalah apa ThrowIfCancellationRequested), maka ketika tugas melihat itu OperationCanceledException, memeriksa apakah OperationCanceledExceptiontoken cocok dengan token Task. Jika ya, pengecualian itu dipandang sebagai pengakuan atas pembatalan kerja sama dan Tasktransisi ke Canceled negara (bukan Faultednegara).
Max Galkin
sumber
2
TPL dipikirkan dengan sangat baik.
Kolonel Panic
1
Saya berasumsi manfaat 1 berlaku sama dengan melewati token pembatalan ke Parallel.ForatauParallel.ForEach
Kolonel Panic
27

Konstruktor menggunakan token untuk penanganan pembatalan secara internal. Jika kode Anda ingin akses ke token, Anda bertanggung jawab untuk meneruskannya sendiri. Saya akan sangat menyarankan membaca Pemrograman Paralel dengan buku Microsoft .NET di CodePlex .

Contoh penggunaan CTS dari buku:

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

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

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

// ... elsewhere ...
cts.Cancel();
pengguna7116
sumber
3
dan apa yang terjadi jika Anda tidak memberikan token sebagai parameter? Sepertinya perilaku akan sama, tidak ada tujuan.
sergtk
2
@ sergdev: Anda melewatkan token untuk mendaftarkannya dengan tugas dan penjadwal. Tidak melewatinya dan menggunakannya akan menjadi perilaku yang tidak terdefinisi.
user7116
3
@sergdev: setelah pengujian: myTask.IsCanceled dan myTask.Status tidak sama ketika Anda tidak melewatkan token sebagai parameter. Status akan gagal bukannya dibatalkan. Meskipun demikian pengecualiannya sama: ini adalah OperationCanceledException dalam kedua kasus tersebut.
Olivier de Rivoyre
2
Bagaimana jika saya tidak menelepon token.ThrowIfCancellationRequested();? Dalam pengujian saya, perilakunya sama. Ada ide?
machinarium
1
@CobaltBlue: when cts.Cancel() is called the Task is going to get canceled and end, no matter what you donggak. Jika Tugas dibatalkan sebelum dimulai, maka dibatalkan . Jika badan Tugas tidak pernah memeriksa token apa pun, itu akan berjalan sampai selesai, menghasilkan status RanToCompletion . Jika tubuh melempar OperationCancelledException, misalnya dengan ThrowIfCancellationRequested, maka Tugas akan memeriksa apakah PembatalanToken Pengecualian itu sama dengan yang terkait dengan Tugas. Jika ya, tugas dibatalkan . Jika tidak, itu menyalahkan .
Wolfzoon
7

Pembatalan bukanlah perkara sederhana seperti yang mungkin dipikirkan banyak orang. Beberapa seluk-beluk dijelaskan dalam posting blog ini di msdn:

Sebagai contoh:

Dalam situasi tertentu dalam Ekstensi Paralel dan dalam sistem lain, perlu untuk membangunkan metode yang diblokir karena alasan yang bukan karena pembatalan eksplisit oleh pengguna. Sebagai contoh, jika satu utas diblokir blockingCollection.Take()karena koleksi sedang kosong dan utas lainnya selanjutnya memanggil blockingCollection.CompleteAdding(), maka panggilan pertama harus bangun dan melempar InvalidOperationExceptionuntuk mewakili penggunaan yang salah.

Pembatalan dalam Ekstensi Paralel

x0n
sumber
4

Berikut adalah contoh yang menunjukkan dua poin dalam jawaban oleh 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;
            }
        }
    }
}

Keluaran:

*********************************************************************
* 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
sumber