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