Skip to main content

6.5 — 3. CancellationToken

Summary

CancellationToken là một tín hiệu hợp tác: nó không giết được thread nào, chỉ báo "bên gọi không cần kết quả nữa" và trông chờ code phía dưới tự nhìn vào rồi dừng. Bên phát tín hiệu là CancellationTokenSource, bên nhận là CancellationToken — tách đôi để hàm nhận token không thể tự huỷ công việc của người khác. Trong ASP.NET Core, tham số CancellationToken của action được bind thẳng vào HttpContext.RequestAborted, nên chỉ cần truyền nó xuống EF Core và HttpClient là request bị client bỏ ngang sẽ kéo theo việc huỷ câu query. Ba lỗi phổ biến nhất: nuốt OperationCanceledException rồi trả về null, truyền token vào cả bước ghi đã đi quá điểm không thể quay lui, và quên Dispose cái CancellationTokenSource có timeout.

Mục tiêu bài học​

Sau bài này bạn có thể:

  • Giải thích vì sao huỷ trong .NET là hợp tác chứ không phải cưỡng chế, và điều đó ràng buộc gì lên code của bạn.
  • Chọn đúng một trong ba cách phản ứng với token: kiểm tra IsCancellationRequested, ném bằng ThrowIfCancellationRequested(), hoặc đăng ký callback.
  • Nối token của HTTP request với token timeout của riêng bạn bằng CreateLinkedTokenSource, và biết bên nào đã kích hoạt.
  • Chỉ ra được ba lỗi khiến việc huỷ thành vô dụng — hoặc tệ hơn, thành nguồn gốc dữ liệu không nhất quán.

Nội dung bài học​

6.5.1 — Vấn đề: client bỏ đi, server không biết​

Một request lấy báo cáo doanh thu trong CRM chạy mất 40 giây. Người dùng đợi 5 giây rồi đóng tab.

Nếu không có token, server vẫn:

  • giữ một thread của thread pool suốt phần còn lại của pipeline,
  • giữ một connection trong connection pool của database,
  • bắt SQL Server chạy nốt câu query không ai còn cần,
  • rồi ghi kết quả vào một socket đã đóng.

Với một người dùng thì không sao. Với 200 người dùng sốt ruột bấm F5, connection pool cạn và những request hoàn toàn bình thường bắt đầu timeout. Đây là kiểu sự cố rất khó chẩn đoán, vì nguyên nhân nằm ở những request đã chết chứ không nằm ở request đang lỗi.

6.5.2 — Huỷ trong .NET là hợp tác, không phải cưỡng chế​

Đây là điểm quan trọng nhất của cả bài, và cũng là điểm hay bị hiểu sai nhất.

token.Cancel() không dừng được thread nào. .NET đã bỏ Thread.Abort vì việc cưỡng chế dừng một thread ở vị trí bất kỳ có thể để lại lock chưa nhả và dữ liệu dở dang. Thay vào đó, huỷ là một quy ước:

Bên gọi giơ cờ. Bên được gọi phải tự nhìn vào lá cờ đó, và tự quyết định dừng ở một điểm an toàn.

Hệ quả trực tiếp: một vòng lặp tính toán không nhìn token thì không bao giờ bị huỷ, dù bạn có truyền token vào bao nhiêu tầng đi nữa. Token chỉ có tác dụng ở nơi có ai đó thật sự đọc nó — hoặc ở các API của .NET đã đọc sẵn giùm bạn (EF Core, HttpClient, Stream, Channel...).

6.5.3 — Hai nửa: CancellationTokenSource và CancellationToken​

// Bên PHÁT tín hiệu — giữ quyền huỷ
using var cts = new CancellationTokenSource();

// Bên NHẬN tín hiệu — chỉ đọc được, không huỷ được
CancellationToken token = cts.Token;

cts.Cancel(); // chỉ chủ sở hữu cts mới làm được
Console.WriteLine(token.IsCancellationRequested); // True

Việc tách đôi này là cố ý. Hàm nhận CancellationToken chỉ quan sát, không thể huỷ công việc của người gọi mình. Nếu một hàm cần quyền huỷ, nó phải tự tạo CancellationTokenSource riêng.

Vài dạng khởi tạo hay dùng:

// Tự huỷ sau 5 giây
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

// Đặt hạn sau khi đã tạo
cts.CancelAfter(TimeSpan.FromSeconds(30));

// .NET 8 trở lên: đợi mọi callback chạy xong rồi mới trả về
await cts.CancelAsync();

CancellationTokenSource có timeout dùng một Timer bên trong, nên nó là disposable thật sự chứ không phải hình thức. Quên using là rò rỉ timer — thứ chỉ lộ ra khi service chạy nhiều ngày.

CancellationToken.None là token không bao giờ bị huỷ; default(CancellationToken) cho ra đúng giá trị đó. Truyền CancellationToken.None là cách nói rõ ràng "chỗ này cố ý không cho huỷ", tốt hơn nhiều so với để trống.

6.5.4 — Ba cách phản ứng với token​

Cách 1 — Kiểm tra cờ, dùng cho vòng lặp mà bạn muốn kết thúc êm:

while (!token.IsCancellationRequested)
{
await ProcessNextBatchAsync(token);
}
// Chạy tới đây nghĩa là đã dừng bình thường, không có exception nào.

Cách 2 — Ném exception, dùng cho phần lớn trường hợp còn lại:

foreach (var customer in customers)
{
token.ThrowIfCancellationRequested(); // ném OperationCanceledException
await SyncAsync(customer, token);
}

Cách này đúng hơn cách 1 khi hàm trả về giá trị: nếu bị huỷ giữa chừng mà vẫn return bình thường, bên gọi sẽ nhận một kết quả thiếu dữ liệu mà không biết là thiếu.

Cách 3 — Đăng ký callback, dùng khi phải dọn dẹp tài nguyên bên ngoài:

using var registration = token.Register(() => connection.Abort());

Register trả về một CancellationTokenRegistration cần được Dispose. Với token sống lâu (ví dụ token shutdown của cả ứng dụng) mà bạn đăng ký callback theo từng request, quên dispose là rò rỉ bộ nhớ tích luỹ dần.

6.5.5 — Token trong ASP.NET Core​

ASP.NET Core bind tham số CancellationToken của action thẳng vào HttpContext.RequestAborted. Bạn không cần cấu hình gì:

[HttpGet("{id}/report")]
public async Task<IActionResult> GetReport(int id, CancellationToken ct)
{
// ct chính là HttpContext.RequestAborted
var report = await _reportService.GenerateAsync(id, ct);
return Ok(report);
}

Nguyên tắc duy nhất cần nhớ: truyền tiếp xuống mọi lời gọi I/O. Token dừng ở tầng nào thì phía dưới tầng đó không huỷ được.

public async Task<CustomerReport> GenerateAsync(int customerId, CancellationToken ct)
{
var customer = await _repo.GetByIdAsync(customerId, ct);
var interactions = await _analytics.GetInteractionsAsync(customerId, ct);
var revenue = await _analytics.GetRevenueAsync(customerId, ct);

return new CustomerReport(customer!, interactions, revenue);
}

Ở tầng dữ liệu, EF Core đẩy token xuống tận DbCommand, nên câu query đang chạy trên SQL Server thật sự bị huỷ chứ không chỉ bị bỏ mặc:

public Task<Customer?> GetByIdAsync(int id, CancellationToken ct) =>
_db.Customers
.Include(c => c.Interactions)
.FirstOrDefaultAsync(c => c.Id == id, ct);

Chú ý là ở đây không có try/catch. Phần sau sẽ giải thích vì sao.

Với BackgroundService, token bạn nhận mang ý nghĩa khác — nó là tín hiệu ứng dụng đang tắt, không phải "một request bị huỷ":

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}

6.5.6 — Nối token: vừa theo client, vừa có hạn của riêng mình​

Thường bạn muốn cả hai điều kiện: huỷ khi client bỏ đi, và huỷ khi quá 10 giây dù client vẫn đợi.

using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeout.Token);

try
{
return await _slowApi.GetAsync(linked.Token);
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
// Hết hạn do ta đặt ra -> đây là lỗi, cần trả về 504 và ghi log
throw new TimeoutException("Nguồn dữ liệu ngoài không phản hồi trong 10 giây.");
}
catch (OperationCanceledException)
{
// Client bỏ đi -> không phải lỗi, không log như lỗi
throw;
}

Mẫu when (timeout.IsCancellationRequested) là cách phân biệt ai đã bấm nút huỷ. Không có nó, hai tình huống rất khác nhau — một cái là sự cố hạ tầng, một cái là hành vi bình thường của người dùng — sẽ trông y hệt nhau trong log.

6.5.7 — Ba lỗi làm hỏng toàn bộ ý nghĩa của việc huỷ​

Lỗi 1 — Nuốt exception rồi trả về giá trị trông như hợp lệ.

// SAI
try
{
return await _db.Customers.FirstOrDefaultAsync(c => c.Id == id, ct);
}
catch (OperationCanceledException)
{
return null; // bên gọi tưởng "không tìm thấy khách hàng"
}

Bên gọi bây giờ không phân biệt được "không có khách hàng này" với "tôi đã bỏ cuộc giữa chừng". Nếu giá trị đó đi tiếp vào một câu lệnh ghi, bạn vừa biến một lần huỷ vô hại thành mất dữ liệu.

Quy tắc: OperationCanceledException để cho nó bay lên. Nó không phải lỗi cần xử lý, nó là câu trả lời đúng cho câu hỏi "công việc kết thúc thế nào".

Lỗi 2 — Bắt Exception chung rồi log như sự cố.

catch (Exception ex)
{
_logger.LogError(ex, "Lấy báo cáo thất bại"); // mỗi lần user đóng tab là một dòng lỗi
}

TaskCanceledException kế thừa từ OperationCanceledException, và cả hai đều lọt vào catch (Exception). Trên một site có lượt truy cập thật, việc này làm ngập dashboard cảnh báo bằng những sự kiện hoàn toàn bình thường, tới mức lỗi thật bị chìm mất. Bắt riêng OperationCanceledException trước, rồi mới tới Exception.

Lỗi 3 — Cho phép huỷ ở giai đoạn đã đi quá điểm không thể quay lui.

var payment = await _gateway.ChargeAsync(order.Total, ct);   // tiền đã bị trừ
await _db.SaveChangesAsync(ct); // huỷ ở đây -> đã thu tiền mà không có đơn

Sau khi một tác dụng phụ không thể hoàn tác đã xảy ra, việc huỷ không còn miễn phí nữa. Giai đoạn ghi kết sổ nên dùng token riêng có hạn ngắn, hoặc CancellationToken.None, kèm một thao tác idempotent để có thể chạy lại an toàn:

var payment = await _gateway.ChargeAsync(order.Total, ct);

// Qua điểm này thì không nhận lệnh huỷ từ client nữa
using var commit = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await _db.SaveChangesAsync(commit.Token);

6.5.8 — Rà lại code của bạn​

Danh sách rà soát CancellationToken

  • •Mọi phương thức async có I/O đều nhận CancellationToken làm tham số cuối.
  • •Token được truyền liên tục từ controller xuống tới EF Core và HttpClient, không đứt ở tầng service.
  • •Không có chỗ nào catch OperationCanceledException rồi trả về giá trị mặc định.
  • •Khối catch (Exception) nào cũng có catch (OperationCanceledException) đứng trước.
  • •Mọi CancellationTokenSource có timeout đều nằm trong using.
  • •Vòng lặp tính toán dài có gọi ThrowIfCancellationRequested ở đầu mỗi vòng.
  • •Giai đoạn ghi sau một tác dụng phụ không hoàn tác được thì không dùng token của client.

Bài tập áp dụng​

Bài 1 — Đo tác dụng thật của CancellationToken​

Viết một endpoint gọi Task.Delay(30_000, ct). Gọi bằng curl, bấm Ctrl+C sau 2 giây, quan sát log. Sau đó bỏ ct khỏi Task.Delay rồi làm lại.

Tiêu chí hoàn thành: bạn ghi lại được thời điểm dòng log cuối cùng xuất hiện trong cả hai trường hợp.

Gợi ý và lời giải — Bài 1

Gợi ý. ASP.NET Core huỷ HttpContext.RequestAborted khi client ngắt kết nối. Nhưng việc huỷ trong .NET là hợp tác — nó chỉ có tác dụng nếu code của bạn thật sự nhận và tôn trọng token.

Lời giải.

app.MapGet("/cham", async (ILogger<Program> log, CancellationToken ct) =>
{
log.LogInformation("BẮT ĐẦU");
try
{
await Task.Delay(30_000, ct); // phiên bản A: có truyền ct
// await Task.Delay(30_000); // phiên bản B: KHÔNG truyền
log.LogInformation("XONG");
return Results.Ok();
}
catch (OperationCanceledException)
{
log.LogWarning("BỊ HUỶ");
throw;
}
});

Thử:

curl http://localhost:5000/cham
# bấm Ctrl+C sau 2 giây

Kết quả:

Có truyền ctKhông truyền ct
Dòng log cuốiBỊ HUỶ tại giây thứ 2XONG tại giây thứ 30
Luồng bị chiếm2 giây30 giây
Kết nối database giữ2 giây30 giây

Ý nghĩa. Ở phiên bản B, client đã bỏ đi từ giây thứ 2, nhưng server vẫn làm việc thêm 28 giây cho một kết quả không ai nhận. Trên hệ thống có tải, phần công việc vô ích này tích tụ rất nhanh.

Vì sao "hợp tác" lại là thiết kế đúng. .NET không có cơ chế giết một luồng đang chạy giữa chừng — và đó là chủ ý. Cưỡng chế dừng sẽ để lại trạng thái dở dang: giao dịch chưa commit, file ghi được nửa chừng, khoá chưa nhả. Vì vậy mỗi thao tác phải tự kiểm tra xem có nên dừng không, tại những điểm mà việc dừng là an toàn.

Ba cách phản ứng với token, tuỳ ngữ cảnh:

// 1. Truyền xuống — cách dùng phổ biến nhất, và nên là mặc định
await _db.Customers.ToListAsync(ct);

// 2. Kiểm tra thủ công — trong vòng lặp tính toán dài
foreach (var item in largeList)
{
ct.ThrowIfCancellationRequested();
Process(item);
}

// 3. Đăng ký hàm gọi lại — khi cần dọn dẹp riêng
using var registration = ct.Register(() => _connection.Cancel());

Quy tắc. Mọi phương thức async trong hệ thống nên nhận CancellationToken làm tham số cuối cùng và truyền nó xuống mọi lời gọi async bên trong. Thêm tham số này từ đầu gần như miễn phí; thêm sau khi đã có hàng trăm phương thức là một dự án nhỏ.

Một lưu ý về xử lý ngoại lệ. Khi bị huỷ, ASP.NET Core không ghi nó như một lỗi 500 — nó nhận ra OperationCanceledException gắn với RequestAborted và bỏ qua. Nhưng nếu bạn tự bắt rồi ném lại một ngoại lệ khác, hệ thống giám sát sẽ báo động giả. Đây chính là lỗi mà mục 6.5.7 cảnh báo.

Bài 2 — Phân biệt hai nguồn huỷ​

Cài đặt mẫu CreateLinkedTokenSource, rồi tạo hai tình huống: client ngắt, và timeout kích hoạt. Endpoint phải phản ứng khác nhau với mỗi trường hợp.

Tiêu chí hoàn thành: hai tình huống cho hai mã trạng thái khác nhau, và bạn nêu được vì sao phân biệt chúng lại quan trọng.

Gợi ý và lời giải — Bài 2

Gợi ý. Khi nối hai token, token kết quả bị huỷ nếu bất kỳ token nguồn nào bị huỷ. Để biết nguồn nào đã kích hoạt, hãy hỏi từng token nguồn sau khi bắt được ngoại lệ.

Lời giải.

app.MapGet("/bao-cao", async (HttpContext ctx, ILogger<Program> log, CancellationToken clientToken) =>
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(clientToken, timeoutCts.Token);

try
{
var report = await GenerateReportAsync(linkedCts.Token);
return Results.Ok(report);
}
catch (OperationCanceledException)
{
if (clientToken.IsCancellationRequested)
{
log.LogInformation("Client đã ngắt kết nối");
return Results.StatusCode(499); // client bỏ đi
}

log.LogWarning("Sinh báo cáo quá 10 giây");
return Results.StatusCode(504); // server quá hạn
}
});

Hai tình huống:

# 1. Client ngắt — bấm Ctrl+C sau 2 giây
curl http://localhost:5000/bao-cao
# log: "Client đã ngắt kết nối"

# 2. Timeout — để chạy quá 10 giây
curl -m 60 http://localhost:5000/bao-cao
# HTTP 504, log: "Sinh báo cáo quá 10 giây"

Vì sao phân biệt lại quan trọng — ba lý do cụ thể:

  1. Cảnh báo vận hành. Client ngắt là chuyện bình thường — người dùng đóng tab, mạng chập chờn. Timeout là dấu hiệu hệ thống có vấn đề. Gộp cả hai vào một loại log khiến biểu đồ lỗi đầy nhiễu, và cảnh báo thật bị chìm.

  2. Client cần biết nên làm gì. Với 504, thử lại có thể thành công. Với 499 thì không có ai để thử lại, vì chính client đã bỏ đi.

  3. Chỉ số chất lượng dịch vụ. Nếu tính tỉ lệ lỗi mà gộp cả trường hợp người dùng đóng tab, con số trở nên vô nghĩa.

Lưu ý về mã 499. Đây là mã không chuẩn, do nginx đặt ra cho trường hợp client đóng kết nối. Nó không nằm trong đặc tả HTTP. Thực tế client đã ngắt nên không ai nhận được phản hồi này — giá trị của nó nằm ở log và chỉ số, không nằm ở việc truyền tin.

Mẫu nối token cũng dùng cho ba tình huống khác:

// Hạn riêng cho một lời gọi bên ngoài, ngắn hơn hạn của cả request
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(3));
var kq = await _httpClient.GetAsync(url, cts.Token);

// Dừng tác vụ nền khi ứng dụng tắt
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct, _lifetime.ApplicationStopping);

// Huỷ khi bất kỳ tác vụ song song nào thất bại
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);

Một điều bắt buộc. Luôn Dispose CancellationTokenSource — dùng using. Không làm vậy sẽ rò rỉ, đặc biệt với CancelAfter vì nó tạo một bộ đếm thời gian nội bộ. Đây là một trong những nguồn rò rỉ bộ nhớ âm thầm nhất trong code async.

Bài 3 — Săn lỗi nuốt ngoại lệ huỷ​

Tìm trong codebase mọi chỗ catch (OperationCanceledException) và mọi catch (Exception) bọc quanh code async. Với từng chỗ, trả lời: nếu request bị huỷ tại đúng dòng này, bên gọi có phân biệt được "không có dữ liệu" và "đã bỏ cuộc" không?

Tiêu chí hoàn thành: bạn tìm được ít nhất một chỗ mà việc huỷ bị biến thành một kết quả trông như hợp lệ.

Gợi ý và lời giải — Bài 3

Gợi ý. Tìm bằng:

grep -rn -A3 "catch (OperationCanceledException\|catch (TaskCanceledException\|catch (Exception" --include=*.cs . \
| grep -B1 "return\|= null\|= new\|Empty"

Dấu hiệu nguy hiểm: khối catch trả về một giá trị trông như bình thường thay vì ném lại.

Lời giải — ba mẫu sai, xếp theo mức nghiêm trọng.

Mẫu 1 — biến việc huỷ thành "không có dữ liệu":

// SAI
public async Task<List<Order>> GetOrdersAsync(int id, CancellationToken ct)
{
try { return await _db.Orders.Where(o => o.CustomerId == id).ToListAsync(ct); }
catch (OperationCanceledException) { return []; } // NGUY HIỂM
}

Bên gọi nhận danh sách rỗng và không phân biệt được "khách hàng này không có đơn nào" với "truy vấn bị huỷ giữa chừng". Nếu kết quả đó được dùng để quyết định — ví dụ "không có đơn thì cho phép xoá khách hàng" — bạn vừa xoá nhầm dữ liệu.

Mẫu 2 — ghi log như một lỗi thật:

// SAI — tạo cảnh báo giả
catch (Exception ex)
{
_logger.LogError(ex, "Lỗi khi lấy đơn hàng"); // huỷ cũng rơi vào đây
throw;
}

Mỗi lần người dùng đóng tab là một dòng log mức Error. Hệ thống giám sát báo động, và sau vài tuần không ai còn để ý tới cảnh báo nữa — kể cả cảnh báo thật.

Mẫu 3 — nuốt hoàn toàn:

// SAI NHẤT
catch (Exception) { }

Cách xử lý đúng — phân biệt rõ ràng:

public async Task<List<Order>> GetOrdersAsync(int id, CancellationToken ct)
{
try
{
return await _db.Orders.Where(o => o.CustomerId == id).ToListAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_logger.LogInformation("Truy vấn đơn hàng bị huỷ cho khách {Id}", id);
throw; // ném lại, KHÔNG nuốt
}
catch (Exception ex)
{
_logger.LogError(ex, "Lỗi khi lấy đơn hàng của khách {Id}", id);
throw;
}
}

Ba điểm quan trọng trong đoạn trên:

  1. Mệnh đề when (ct.IsCancellationRequested). Nó phân biệt việc huỷ có chủ ý với TaskCanceledException do timeout của HttpClient — vốn cũng là một OperationCanceledException nhưng lại là một lỗi thật cần xử lý khác.

  2. Log mức Information, không phải Error. Huỷ là chuyện bình thường.

  3. Luôn ném lại. Bên gọi cần biết thao tác không hoàn tất. ASP.NET Core sẽ nhận ra và không trả 500.

Quy tắc chung. OperationCanceledException không phải lỗi — nó là một tín hiệu điều khiển luồng. Xử lý nó như lỗi tạo ra nhiễu; xử lý nó như kết quả bình thường tạo ra dữ liệu sai. Cách đúng duy nhất là ghi nhận rồi để nó đi tiếp.

Một chỗ hiếm hoi được phép nuốt. Trong vòng lặp của một BackgroundService, khi ứng dụng đang tắt:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessOneCycleAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Ứng dụng đang tắt — đây là kết thúc bình thường, không ném lại
}
}

Ở đây không có bên gọi nào cần biết, và việc huỷ chính là cách dừng được thiết kế sẵn. Module 14 trình bày đầy đủ vòng đời của tác vụ nền.

Tự kiểm tra​

Frequently asked questions

Vì sao truyền CancellationToken vào một vòng lặp tính toán mà nó vẫn chạy tới hết?

Vì huỷ trong .NET là hợp tác. Token chỉ là một lá cờ; không có gì tự động ngắt thread. Vòng lặp phải tự gọi token.ThrowIfCancellationRequested() hoặc kiểm tra token.IsCancellationRequested ở mỗi vòng. Các API của .NET như EF Core hay HttpClient huỷ được là vì bản thân chúng đã đọc token giùm bạn.

Khác nhau giữa OperationCanceledException và TaskCanceledException?

TaskCanceledException kế thừa từ OperationCanceledException. Nên bắt OperationCanceledException là bắt được cả hai. Điều này cũng có nghĩa là catch (Exception) sẽ nuốt luôn tín hiệu huỷ — đó là lý do phải đặt catch (OperationCanceledException) lên trước.

Trong ASP.NET Core, tham số CancellationToken của action đến từ đâu?

Từ HttpContext.RequestAborted, được framework bind tự động, không cần cấu hình. Nó phát tín hiệu khi kết nối của client bị đóng trước lúc response hoàn tất.

Khi nào KHÔNG nên truyền token của client xuống dưới?

Khi đã xảy ra một tác dụng phụ không hoàn tác được — ví dụ đã gọi cổng thanh toán thành công. Từ điểm đó, huỷ giữa chừng để lại hệ thống ở trạng thái không nhất quán. Giai đoạn kết sổ nên dùng token riêng có hạn ngắn hoặc CancellationToken.None, và nên idempotent để chạy lại an toàn.

Vì sao CancellationTokenSource cần Dispose?

Bản có timeout (khởi tạo bằng TimeSpan, hoặc gọi CancelAfter) dùng một Timer bên trong. Không dispose thì timer còn sống, và trên một service chạy nhiều ngày đó là rò rỉ tích luỹ dần. CreateLinkedTokenSource cũng vậy: nó đăng ký callback lên các token nguồn, dispose mới gỡ ra.

Làm sao biết request bị huỷ do client bỏ đi hay do timeout của mình?

Dùng CreateLinkedTokenSource và giữ lại tham chiếu tới CancellationTokenSource timeout, rồi lọc bằng exception filter: catch (OperationCanceledException) when (timeout.IsCancellationRequested). Nhánh đó là timeout — một sự cố cần log và trả 504. Nhánh còn lại là client bỏ đi — hành vi bình thường, không log như lỗi.

Kết luận​

Ba điều đáng nhớ nhất từ bài này:

  1. Token là lá cờ, không phải cái phanh. Nó chỉ có tác dụng ở nơi có ai đó đọc nó. Truyền token qua mười tầng mà tầng cuối không đọc thì mười tầng kia đều vô nghĩa.
  2. Để OperationCanceledException bay lên. Nuốt nó đi là xoá mất thông tin duy nhất phân biệt "không có dữ liệu" với "đã bỏ cuộc giữa chừng".
  3. Huỷ không còn miễn phí sau tác dụng phụ đầu tiên. Trước khi gọi cổng thanh toán, huỷ là chuyện nhỏ. Sau đó thì không.

Tham khảo​

Điều hướng​

Bài liên quan​