1. Làm quen với CancellationToken
Hãy tưởng tượng tình huống: bạn bắt đầu tải xuống một file lớn, rồi bỗng nhớ ra mạng Internet của bạn theo gói tính phí và mỗi gigabyte đều quý giá. Hoặc người dùng bối rối, khởi chạy một phép tính rồi quyết định là không cần nữa. Họ muốn nhấn "Hủy" và không chờ hoàn tất. Ứng dụng hiện đại phải phản hồi tốt, và để làm được điều đó chúng ta cần biết cách truyền tín hiệu dừng bất cứ lúc nào và ngắt đúng cách thao tác bất đồng bộ.
Vì lý do đó trong .NET có cả một hạ tầng hủy — CancellationToken.
CancellationToken (nghĩa đen là "token hủy") là một đối tượng đặc biệt bạn truyền vào trong thao tác kéo dài. Bất kỳ lúc nào bạn có thể báo hủy qua CancellationTokenSource, và thao tác nên định kỳ kiểm tra token (ví dụ IsCancellationRequested) và kết thúc đúng cách, khi cần gọi ThrowIfCancellationRequested().
Nó hoạt động như thế nào? (Ngắn gọn)
- Có một đối tượng CancellationTokenSource tạo ra token và có thể hủy chúng (phương thức Cancel()).
- Bản thân CancellationToken là "cờ báo hiệu" có thể phát cho nhiều thao tác (qua thuộc tính Token của source).
- Thao tác định kỳ kiểm tra token: nếu hủy được yêu cầu, thoát khỏi công việc (hoặc ném OperationCanceledException).
Ẩn dụ: bạn là sếp (bạn chính là CancellationTokenSource). Bạn phát cho nhân viên "thẻ" (đó là CancellationToken). Khi bạn quyết định hủy mọi thứ, bạn giơ cờ đỏ — và tất cả ai có thẻ sẽ rút lui ngay, bỏ dở bữa trưa.
2. Cách sử dụng CancellationToken
Tạo nguồn token hủy (CancellationTokenSource)
var cts = new CancellationTokenSource();
Lấy chính token (CancellationToken)
CancellationToken token = cts.Token;
Truyền token vào phương thức bất đồng bộ
Hầu hết các phương thức bất đồng bộ chuẩn của .NET chấp nhận tham số kiểu CancellationToken. Ví dụ, HttpClient.GetAsync, Stream.ReadAsync, Task.Delay v.v.
Ví dụ — delay có thể hủy:
await Task.Delay(10000, token); // Chờ 10 giây — nhưng có thể hủy!
Yêu cầu hủy (ví dụ, theo nút bấm hoặc timer)
cts.Cancel(); // Tất cả các thao tác nhận token này sẽ biết là đã hủy
Kiểm tra token bên trong phương thức
Trong các phương thức của bạn (nhất là khi công việc dài và lặp) cần thường xuyên kiểm tra cờ hủy và ném ngoại lệ OperationCanceledException nếu hủy được yêu cầu:
token.ThrowIfCancellationRequested();
Hoặc chỉ đơn giản kiểm tra thuộc tính:
if (token.IsCancellationRequested)
{
// Giải phóng tài nguyên, thoát khỏi phương thức
}
3. Ví dụ: Thêm hủy vào ứng dụng học tập của chúng ta
Giả sử chúng ta có một ứng dụng tải dữ liệu từ website. Thêm tính năng hủy tải nếu người dùng đổi ý.
Ví dụ cơ bản tải bất đồng bộ
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class Downloader
{
public async Task DownloadAsync(string url)
{
var client = new HttpClient();
string content = await client.GetStringAsync(url); // Không có hủy
Console.WriteLine("Tải xong!");
}
}
Thêm CancellationToken
public async Task DownloadAsync(string url, CancellationToken token)
{
var client = new HttpClient();
string content = await client.GetStringAsync(url, token); // Giờ hỗ trợ hủy!
Console.WriteLine("Tải xong!");
}
Quản lý hủy từ phía gọi
static async Task Main(string[] args)
{
var downloader = new Downloader();
var cts = new CancellationTokenSource();
Console.WriteLine("Nhập URL để tải:");
string url = Console.ReadLine();
var downloadTask = downloader.DownloadAsync(url, cts.Token);
Console.WriteLine("Nhấn phím bất kỳ để hủy tải...");
Console.ReadKey();
cts.Cancel(); // Tín hiệu hủy
try
{
await downloadTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("Tải đã bị người dùng hủy!");
}
}
Rất đơn giản! Giờ người dùng có thể dừng thao tác bất cứ lúc nào.
4. Tương tác giữa CancellationTokenSource và các phương thức
flowchart TD
A["Mã người dùng (Main)"] -- tạo --> B["CancellationTokenSource"]
B -- phát --> C["CancellationToken"]
C -- truyền --> D["Thao tác bất đồng bộ"]
A -- gọi Cancel() --> B
D -- định kỳ kiểm tra --> C
C -- báo hủy --> D
D -- ném Exception hoặc kết thúc công việc --> A
5. Xử lý hủy
Khi thao tác nhận token hủy, có hai cách hành xử:
Các phương thức .NET tự ném ngoại lệ.
Nếu bạn gọi các phương thức chuẩn như Stream.ReadAsync, HttpClient.GetAsync hoặc Task.Delay và truyền token, — ngay khi Cancel() được gọi, những phương thức này sẽ tự ném OperationCanceledException. Bạn chỉ cần bắt ngoại lệ này.
Mã bất đồng bộ do người dùng tự viết.
Nếu bạn tự triển khai một thao tác "dài", ví dụ xử lý lặp hoặc tính toán nặng, trách nhiệm của bạn là thường xuyên kiểm tra token.IsCancellationRequested (hoặc gọi token.ThrowIfCancellationRequested()) để phản hồi hủy đúng cách.
Ví dụ: thao tác "dài" kiểm tra hủy thủ công
public async Task CalculatePrimesAsync(int max, CancellationToken token)
{
for (int i = 2; i < max; i++)
{
token.ThrowIfCancellationRequested(); // Kiểm tra hủy
if (IsPrime(i))
{
Console.WriteLine($"Số nguyên tố: {i}");
await Task.Delay(100, token); // Cho "nghỉ" (có thể hủy)
}
}
Console.WriteLine("Tính toán hoàn tất!");
}
private bool IsPrime(int n)
{
for (int i = 2; i <= Math.Sqrt(n); i++)
if (n % i == 0) return false;
return true;
}
6. Các điểm hữu ích cần lưu ý
Phương thức và lớp hỗ trợ CancellationToken
| Lớp/phương thức | Hỗ trợ CancellationToken? | Ví dụ sử dụng |
|---|---|---|
|
✔ | |
|
✔ | |
|
✔ | |
|
✔ | |
|
✔ | |
|
✖ | Không hỗ trợ; tốt hơn là dùng Task.Delay |
| Các phương thức của bạn | ✔ (nếu bạn thêm hỗ trợ!) | |
Vòng đời của một thao tác có thể hủy
sequenceDiagram
participant Người_Dùng
participant Main
participant CancellationTokenSource
participant Thao_tác_bất_đồng_bộ
Người_Dùng->>Main: Khởi chạy thao tác
Main->>CancellationTokenSource: Tạo CTS
Main->>Thao_tác_bất_đồng_bộ: Khởi chạy và truyền CancellationToken
Người_Dùng->>Main: Nhấn "Hủy"
Main->>CancellationTokenSource: Gọi Cancel()
Thao_tác_bất_đồng_bộ->>Thao_tác_bất_đồng_bộ: Nhận thấy hủy (\nIsCancellationRequested)
Thao_tác_bất_đồng_bộ-->>Main: Ném OperationCanceledException
Main->>Người_Dùng: Hiển thị thông báo "Thao tác đã bị hủy"
Nơi sử dụng trong đời thực
- Ứng dụng UI: Dừng tải dài, tính toán, thao tác file nếu người dùng muốn đóng cửa sổ hoặc hủy hành động.
- Ứng dụng server: Nếu client cắt kết nối — tốt nhất là hủy xử lý request ngay để không lãng phí tài nguyên.
- Xử lý dữ liệu lớn: Các tác vụ có thể rất dài — luôn cần có khả năng dừng tính toán hoặc migration.
- Tích hợp phần cứng: Quét, in ấn và các thao tác khác đôi khi cần dừng khẩn cấp — hỗ trợ hủy ở đây là bắt buộc.
7. Các lỗi thường gặp khi làm việc với CancellationToken
Lỗi #1: Bỏ qua kiểm tra token.
Nếu thao tác không kiểm tra token.IsCancellationRequested hoặc không gọi ThrowIfCancellationRequested(), nó sẽ không dừng khi hủy, tiêu tốn tài nguyên.
Lỗi #2: Xử lý sai OperationCanceledException.
Nếu không bắt OperationCanceledException, ứng dụng có thể kết thúc bất ngờ. Luôn dùng try-catch để xử lý hủy.
Lỗi #3: Quản lý tài nguyên không đúng khi hủy.
Hủy không tự hoàn tác thay đổi (ví dụ, file hay DB). Cần tự dọn dẹp tài nguyên trong khối catch.
Lỗi #4: Truyền token đã bị hủy.
Nếu token đã bị hủy, phương thức sẽ ném ngay ngoại lệ, điều này có thể làm hỏng luồng nếu không được tính tới.
GO TO FULL VERSION