Pembatalan di Utas Terkelola

Dimulai dengan .NET Framework 4, .NET menggunakan model terpadu untuk pembatalan kooperatif operasi asinkron atau sinkron yang berjalan lama. Model ini didasarkan pada objek ringan yang disebut token pembatalan. Objek yang memanggil satu atau lebih operasi yang dapat dibatalkan, misalnya dengan membuat utas atau tugas baru, meneruskan token ke setiap operasi. Operasi individual pada gilirannya dapat meneruskan salinan token ke operasi lain. Di lain waktu, objek yang membuat token dapat menggunakannya untuk meminta operasi menghentikan apa yang sedang dilakukan. Hanya objek yang meminta yang dapat mengeluarkan permintaan pembatalan, dan setiap pendengar bertanggung jawab untuk memperhatikan permintaan dan menanggapinya dengan cara dan waktu yang tepat.

Pola umum untuk menerapkan model pembatalan kooperatif adalah:

  • Membuat instans objek CancellationTokenSource, yang mengelola dan mengirim pemberitahuan pembatalan ke token pembatalan individu.

  • Teruskan token yang dikembalikan oleh properti CancellationTokenSource.Token ke setiap tugas atau utas yang mendengarkan pembatalan.

  • Menyediakan mekanisme untuk setiap tugas atau utas untuk merespons pembatalan.

  • Panggil metode CancellationTokenSource.Cancel untuk memberikan pemberitahuan pembatalan.

Penting

Kelas CancellationTokenSource menerapkan antarmuka IDisposable. Anda harus memastikan untuk memanggil metode CancellationTokenSource.Dispose ketika Anda telah selesai menggunakan sumber token pembatalan untuk membebaskan sumber daya yang tidak dikelola yang dimilikinya.

Ilustrasi berikut menunjukkan hubungan antara sumber token dan semua salinan tokennya.

CancellationTokenSource and cancellation tokens

Model pembatalan kooperatif memudahkan untuk membuat aplikasi dan pustaka yang sadar-pembatalan, dan mendukung fitur-fitur berikut:

  • Pembatalan bersifat kooperatif dan tidak dipaksakan kepada pendengar. Pendengar menentukan cara mengakhiri dengan baik sebagai respons terhadap permintaan pembatalan.

  • Meminta berbeda dengan mendengarkan. Objek yang memanggil operasi yang dapat dibatalkan dapat mengontrol kapan (jika terjadi) pembatalan diminta.

  • Objek yang meminta mengeluarkan permintaan pembatalan ke semua salinan token hanya dengan menggunakan satu panggilan metode.

  • Pendengar dapat mendengarkan beberapa token secara bersamaan dengan menggabungkannya ke dalam satu token terhubung.

  • Kode pengguna dapat mengetahui dan menanggapi permintaan pembatalan dari kode pustaka, dan kode pustaka dapat mengetahui dan menanggapi permintaan pembatalan dari kode pengguna.

  • Pendengar dapat diberi tahu tentang permintaan pembatalan dengan polling, pendaftaran panggilan balik, atau menunggu handel tunggu.

Jenis Pembatalan

Kerangka kerja pembatalan diimplementasikan sebagai satu set jenis terkait, yang tercantum dalam tabel berikut.

Nama jenis Deskripsi
CancellationTokenSource Objek yang membuat token pembatalan, dan juga mengeluarkan permintaan pembatalan untuk semua salinan token tersebut.
CancellationToken Jenis nilai ringan yang diteruskan ke satu atau beberapa pendengar, biasanya sebagai parameter metode. Pendengar memantau nilai properti IsCancellationRequested token dengan polling, panggilan balik, atau handel tunggu.
OperationCanceledException Kelebihan beban konstruktor pengecualian ini menerima CancellationToken sebagai parameter. Pendengar dapat secara opsional menampilkan pengecualian ini untuk memverifikasi sumber pembatalan dan memberi tahu yang lainnya bahwa sumber telah menanggapi permintaan pembatalan.

Model pembatalan diintegrasikan ke dalam .NET dalam beberapa jenis. Yang paling penting adalah System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult>, dan System.Linq.ParallelEnumerable. Kami menyarankan agar Anda menggunakan model pembatalan kooperatif ini untuk semua pustaka baru dan kode aplikasi baru.

Contoh Kode

Dalam contoh berikut, objek yang meminta membuat objek CancellationTokenSource, lalu meneruskan properti Token ke operasi yang dapat dibatalkan. Operasi yang menerima permintaan memantau nilai properti IsCancellationRequested dari token dengan polling. Ketika nilai menjadi true, pendengar dapat mengakhiri dengan cara apa pun yang sesuai. Dalam contoh ini, metode hanya keluar, yang mana hanya itu yang diperlukan dalam banyak kasus.

Catatan

Contoh menggunakan metode QueueUserWorkItem untuk menunjukkan bahwa kerangka kerja pembatalan kooperatif cocok dengan API lama. Untuk contoh yang menggunakan jenis System.Threading.Tasks.Task pilihan, lihat Cara: Membatalkan Tugas dan Turunannya.

using System;
using System.Threading;

public class Example
{
    public static void Main()
    {
        // Create the token source.
        CancellationTokenSource cts = new CancellationTokenSource();

        // Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
        Thread.Sleep(2500);

        // Request cancellation.
        cts.Cancel();
        Console.WriteLine("Cancellation set in token source...");
        Thread.Sleep(2500);
        // Cancellation should have happened, so call Dispose.
        cts.Dispose();
    }

    // Thread 2: The listener
    static void DoSomeWork(object? obj)
    {
        if (obj is null)
            return;

        CancellationToken token = (CancellationToken)obj;

        for (int i = 0; i < 100000; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1);
                // Perform cleanup if necessary.
                //...
                // Terminate the operation.
                break;
            }
            // Simulate some work.
            Thread.SpinWait(500000);
        }
    }
}
// The example displays output like the following:
//       Cancellation set in token source...
//       In iteration 1430, cancellation has been requested...
Imports System.Threading

Module Example
    Public Sub Main()
        ' Create the token source.
        Dim cts As New CancellationTokenSource()

        ' Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
        Thread.Sleep(2500)

        ' Request cancellation by setting a flag on the token.
        cts.Cancel()
        Console.WriteLine("Cancellation set in token source...")
        Thread.Sleep(2500)
        ' Cancellation should have happened, so call Dispose.
        cts.Dispose()
    End Sub

    ' Thread 2: The listener
    Sub DoSomeWork(ByVal obj As Object)
        Dim token As CancellationToken = CType(obj, CancellationToken)

        For i As Integer = 0 To 1000000
            If token.IsCancellationRequested Then
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1)
                ' Perform cleanup if necessary.
                '...
                ' Terminate the operation.
                Exit For
            End If

            ' Simulate some work.
            Thread.SpinWait(500000)
        Next
    End Sub
End Module
' The example displays output like the following:
'       Cancellation set in token source...
'       In iteration 1430, cancellation has been requested...

Pembatalan Operasi Versus Pembatalan Objek

Dalam kerangka kerja pembatalan kooperatif, pembatalan mengacu pada operasi, bukan objek. Permintaan pembatalan berarti bahwa operasi harus berhenti sesegera mungkin setelah pembersihan yang diperlukan dilakukan. Satu token pembatalan harus mengacu pada satu "operasi yang dapat dibatalkan", tetapi operasi tersebut mungkin diterapkan dalam program Anda. Setelah properti IsCancellationRequested dari token diatur ke true, properti tersebut tidak dapat diatur ulang ke false. Karenanya, token pembatalan tidak dapat digunakan kembali setelah dibatalkan.

Jika Anda memerlukan mekanisme pembatalan objek, Anda dapat mendasarkan pada mekanisme pembatalan operasi dengan memanggil metode CancellationToken.Register, seperti yang ditunjukkan dalam contoh berikut.

using System;
using System.Threading;

class CancelableObject
{
    public string id;

    public CancelableObject(string id)
    {
        this.id = id;
    }

    public void Cancel()
    {
        Console.WriteLine("Object {0} Cancel callback", id);
        // Perform object cancellation here.
    }
}

public class Example1
{
    public static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        // User defined Class with its own method for cancellation
        var obj1 = new CancelableObject("1");
        var obj2 = new CancelableObject("2");
        var obj3 = new CancelableObject("3");

        // Register the object's cancel method with the token's
        // cancellation request.
        token.Register(() => obj1.Cancel());
        token.Register(() => obj2.Cancel());
        token.Register(() => obj3.Cancel());

        // Request cancellation on the token.
        cts.Cancel();
        // Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose();
    }
}
// The example displays the following output:
//       Object 3 Cancel callback
//       Object 2 Cancel callback
//       Object 1 Cancel callback
Imports System.Threading

Class CancelableObject
    Public id As String

    Public Sub New(id As String)
        Me.id = id
    End Sub

    Public Sub Cancel()
        Console.WriteLine("Object {0} Cancel callback", id)
        ' Perform object cancellation here.
    End Sub
End Class

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

        ' User defined Class with its own method for cancellation
        Dim obj1 As New CancelableObject("1")
        Dim obj2 As New CancelableObject("2")
        Dim obj3 As New CancelableObject("3")

        ' Register the object's cancel method with the token's
        ' cancellation request.
        token.Register(Sub() obj1.Cancel())
        token.Register(Sub() obj2.Cancel())
        token.Register(Sub() obj3.Cancel())

        ' Request cancellation on the token.
        cts.Cancel()
        ' Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose()
    End Sub
End Module
' The example displays output like the following:
'       Object 3 Cancel callback
'       Object 2 Cancel callback
'       Object 1 Cancel callback

Jika objek mendukung lebih dari satu operasi yang dapat dibatalkan secara bersamaan, teruskan token terpisah sebagai input ke setiap operasi yang dapat dibatalkan yang berbeda. Dengan begitu, satu operasi dapat dibatalkan tanpa memengaruhi yang lain.

Mendengarkan dan Menanggapi Permintaan Pembatalan

Dalam delegasi pengguna, pelaksana operasi yang dapat dibatalkan menentukan cara mengakhiri operasi sebagai respons terhadap permintaan pembatalan. Dalam banyak kasus, delegasi pengguna dapat melakukan pembersihan apa pun yang diperlukan dan kemudian segera kembali.

Namun, dalam kasus yang lebih kompleks, delegasi pengguna mungkin perlu memberi tahu kode pustaka bahwa pembatalan telah terjadi. Dalam kasus seperti itu, cara yang benar untuk mengakhiri operasi adalah delegasi memanggil metode ThrowIfCancellationRequested, yang akan menyebabkan OperationCanceledException ditampilkan. Kode pustaka dapat menangkap pengecualian ini pada utas delegasi pengguna dan memeriksa token pengecualian untuk menentukan apakah pengecualian menunjukkan pembatalan kooperatif atau situasi pengecualian lainnya.

Kelas Task menangani OperationCanceledException dengan cara ini. Untuk informasi selengkapnya, lihat Pembatalan Tugas.

Mendengarkan dengan Polling

Untuk komputasi jangka panjang yang berkali-kali atau berulang, Anda dapat mendengarkan permintaan pembatalan dengan melakukan polling nilai properti CancellationToken.IsCancellationRequestedsecara berkala. Jika nilainya adalah true, metode harus dibersihkan dan diakhiri secepat mungkin. Frekuensi optimal polling bergantung pada jenis aplikasi. Terserah pengembang untuk menentukan frekuensi polling terbaik untuk setiap program apa pun. Polling itu sendiri tidak berdampak signifikan pada performa. Contoh berikut menunjukkan satu cara yang mungkin untuk melakukan poll.

static void NestedLoops(Rectangle rect, CancellationToken token)
{
   for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
      // Assume that we know that the inner loop is very fast.
      // Therefore, polling once per column in the outer loop condition
      // is sufficient.
      for (int row = 0; row < rect.rows; row++) {
         // Simulating work.
         Thread.SpinWait(5_000);
         Console.Write("{0},{1} ", col, row);
      }
   }

   if (token.IsCancellationRequested) {
      // Cleanup or undo here if necessary...
      Console.WriteLine("\r\nOperation canceled");
      Console.WriteLine("Press any key to exit.");

      // If using Task:
      // token.ThrowIfCancellationRequested();
   }
}
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
    Dim col As Integer
    For col = 0 To rect.columns - 1
        ' Assume that we know that the inner loop is very fast.
        ' Therefore, polling once per column in the outer loop condition
        ' is sufficient.
        For col As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", x, y)
        Next
    Next

    If token.IsCancellationRequested = True Then
        ' Cleanup or undo here if necessary...
        Console.WriteLine(vbCrLf + "Operation canceled")
        Console.WriteLine("Press any key to exit.")

        ' If using Task:
        ' token.ThrowIfCancellationRequested()
    End If
End Sub

Untuk contoh yang lebih lengkap, lihat Cara: Mendengarkan Permintaan Pembatalan dengan Polling.

Mendengarkan dengan Mendaftarkan Panggilan Balik

Beberapa operasi dapat diblokir sedemikian rupa sehingga mereka tidak dapat memeriksa nilai token pembatalan pada waktu yang tepat. Untuk kasus ini, Anda dapat mendaftarkan metode panggilan balik yang membuka blokir metode saat permintaan pembatalan diterima.

Metode Register mengembalikan objek CancellationTokenRegistration yang digunakan khusus untuk tujuan ini. Contoh berikut menunjukkan cara menggunakan metode Register untuk membatalkan permintaan Web asinkron.

using System;
using System.Net;
using System.Threading;

class Example4
{
    static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        StartWebRequest(cts.Token);

        // cancellation will cause the web
        // request to be cancelled
        cts.Cancel();
    }

    static void StartWebRequest(CancellationToken token)
    {
        WebClient wc = new WebClient();
        wc.DownloadStringCompleted += (s, e) => Console.WriteLine("Request completed.");

        // Cancellation on the token will
        // call CancelAsync on the WebClient.
        token.Register(() =>
        {
            wc.CancelAsync();
            Console.WriteLine("Request cancelled!");
        });

        Console.WriteLine("Starting request.");
        wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
    }
}
Imports System.Net
Imports System.Threading

Class Example
    Private Shared Sub Main()
        Dim cts As New CancellationTokenSource()

        StartWebRequest(cts.Token)

        ' cancellation will cause the web 
        ' request to be cancelled
        cts.Cancel()
    End Sub

    Private Shared Sub StartWebRequest(token As CancellationToken)
        Dim wc As New WebClient()
        wc.DownloadStringCompleted += Function(s, e) Console.WriteLine("Request completed.")

        ' Cancellation on the token will 
        ' call CancelAsync on the WebClient.
        token.Register(Function()
                           wc.CancelAsync()
                           Console.WriteLine("Request cancelled!")

                       End Function)

        Console.WriteLine("Starting request.")
        wc.DownloadStringAsync(New Uri("http://www.contoso.com"))
    End Sub
End Class

Objek CancellationTokenRegistration mengelola sinkronisasi utas dan memastikan bahwa panggilan balik akan berhenti dijalankan pada titik waktu yang tepat.

Untuk memastikan daya tanggap sistem dan untuk menghindari kebuntuan, panduan berikut harus diikuti saat mendaftarkan panggilan balik:

  • Metode panggilan balik harus cepat karena dipanggil secara sinkron dan karenanya panggilan ke Cancel tidak kembali sampai panggilan balik kembali.

  • Jika Anda memanggil Dispose saat panggilan balik sedang berjalan, lalu menahan kunci yang ditunggu panggilan balik, program Anda dapat mengalami kebuntuan. Setelah Dispose kembali, Anda dapat membebaskan sumber daya apa pun yang diperlukan oleh panggilan balik.

  • Panggilan balik tidak boleh melakukan utas manual atau penggunaan SynchronizationContext apa pun dalam panggilan balik. Jika panggilan balik harus berjalan pada utas tertentu, gunakan konstruktor System.Threading.CancellationTokenRegistration yang memungkinkan Anda menentukan bahwa syncContext target adalah SynchronizationContext.Current yang aktif. Melakukan pengaluran manual dalam panggilan balik dapat menyebabkan kebuntuan.

Untuk contoh yang lebih lengkap, lihat Cara: Mendaftarkan Panggilan Balik untuk Permintaan Pembatalan.

Mendengarkan dengan Menggunakan Handel Tunggu

Ketika operasi yang dapat dibatalkan dapat memblokir saat menunggu primitif sinkronisasi seperti System.Threading.ManualResetEvent atau System.Threading.Semaphore, Anda dapat menggunakan properti CancellationToken.WaitHandle untuk memungkinkan operasi menunggu peristiwa dan permintaan pembatalan. Handel tunggu token pembatalan akan menjadi sinyal sebagai respons terhadap permintaan pembatalan, dan metode dapat menggunakan nilai pengembalian dari metode WaitAny untuk menentukan apakah itu token pembatalan yang memberi sinyal. Operasi kemudian dapat keluar, atau melempar OperationCanceledException, sebagaimana mewajarkan.

// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
       WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                          new TimeSpan(0, 0, 20));
' Wait on the event if it is not signaled.
Dim waitHandles() As WaitHandle = {mre, token.WaitHandle}
Dim eventThatSignaledIndex =
    WaitHandle.WaitAny(waitHandles, _
                       New TimeSpan(0, 0, 20))

System.Threading.ManualResetEventSlim dan System.Threading.SemaphoreSlim keduanya mendukung kerangka kerja pembatalan dalam metodeWait. Anda dapat meneruskan CancellationToken ke metode, dan ketika pembatalan diminta, peristiwa ini bangun dan menampilkan OperationCanceledException.

try
{
    // mres is a ManualResetEventSlim
    mres.Wait(token);
}
catch (OperationCanceledException)
{
    // Throw immediately to be responsive. The
    // alternative is to do one more item of work,
    // and throw on next iteration, because
    // IsCancellationRequested will be true.
    Console.WriteLine("The wait operation was canceled.");
    throw;
}

Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
Try
    ' mres is a ManualResetEventSlim
    mres.Wait(token)
Catch e As OperationCanceledException
    ' Throw immediately to be responsive. The
    ' alternative is to do one more item of work,
    ' and throw on next iteration, because
    ' IsCancellationRequested will be true.
    Console.WriteLine("Canceled while waiting.")
    Throw
End Try

' Simulating work.
Console.Write("Working...")
Thread.SpinWait(500000)

Untuk contoh yang lebih lengkap, lihat Cara: Mendengarkan Permintaan yang Memiliki Handel Tunggu.

Mendengarkan Beberapa Token Secara Bersamaan

Dalam beberapa kasus, pendengar mungkin harus mendengarkan beberapa token pembatalan secara bersamaan. Contohnya, operasi yang dapat dibatalkan mungkin harus memantau token pembatalan internal selain token yang diteruskan secara eksternal sebagai argumen ke parameter metode. Untuk mencapai hal ini, buat sumber token tertaut yang dapat menggabungkan dua atau beberapa token ke dalam satu token, seperti yang ditunjukkan dalam contoh berikut.

public void DoWork(CancellationToken externalToken)
{
    // Create a new token that combines the internal and external tokens.
    this.internalToken = internalTokenSource.Token;
    this.externalToken = externalToken;

    using (CancellationTokenSource linkedCts =
            CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
    {
        try
        {
            DoWorkInternal(linkedCts.Token);
        }
        catch (OperationCanceledException)
        {
            if (internalToken.IsCancellationRequested)
            {
                Console.WriteLine("Operation timed out.");
            }
            else if (externalToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancelling per user request.");
                externalToken.ThrowIfCancellationRequested();
            }
        }
    }
}
Public Sub DoWork(ByVal externalToken As CancellationToken)
    ' Create a new token that combines the internal and external tokens.
    Dim internalToken As CancellationToken = internalTokenSource.Token
    Dim linkedCts As CancellationTokenSource =
    CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)
    Using (linkedCts)
        Try
            DoWorkInternal(linkedCts.Token)
        Catch e As OperationCanceledException
            If e.CancellationToken = internalToken Then
                Console.WriteLine("Operation timed out.")
            ElseIf e.CancellationToken = externalToken Then
                Console.WriteLine("Canceled by external token.")
                externalToken.ThrowIfCancellationRequested()
            End If
        End Try
    End Using
End Sub

Perhatikan bahwa Anda harus memanggil Dispose pada sumber token tertaut ketika Anda selesai dengannya. Untuk contoh yang lebih lengkap, lihat Cara: Mendengarkan Beberapa Permintaan Pembatalan.

Kerja Sama Antara Kode Pustaka dan Kode Pengguna

Kerangka kerja pembatalan terpadu memungkinkan kode pustaka untuk membatalkan kode pengguna, dan untuk kode pengguna membatalkan kode pustaka dengan cara yang kooperatif. Kerja sama yang lancar bergantung pada setiap sisi mengikuti pedoman ini:

  • Jika kode pustaka menyediakan operasi yang dapat dibatalkan, kode pustaka juga harus menyediakan metode publik yang menerima token pembatalan eksternal sehingga kode pengguna dapat meminta pembatalan.

  • Jika kode pustaka memanggil kode pengguna, kode pustaka harus menafsirkan OperationCanceledException(externalToken) sebagai pembatalan kooperatif, dan belum tentu sebagai pengecualian kegagalan.

  • Delegasi pengguna harus mencoba menanggapi permintaan pembatalan dari kode pustaka secara tepat waktu.

System.Threading.Tasks.Task and System.Linq.ParallelEnumerable adalah contoh kelas yang mengikuti panduan ini. Untuk informasi selengkapnya, lihat Pembatalan Tugas dan Cara: Membatalkan Permintaan PLINQ.

Lihat juga