6.4 — 2. ConfigureAwait
ConfigureAwait(false) nói một điều duy nhất: "phần code sau await không cần chạy lại trên context gốc". Nó tồn tại vì SynchronizationContext — cơ chế trong ASP.NET Framework và WinForms/WPF bắt continuation phải quay về đúng một thread. Khi thread đó đang bị .Result hoặc .Wait() block, bạn có một deadlock hoàn hảo: thread chờ Task, Task chờ thread. ASP.NET Core không có SynchronizationContext, nên trong code ứng dụng web ngày nay bạn không cần ConfigureAwait(false) — nhưng trong thư viện thì vẫn cần, vì thư viện không biết ai sẽ gọi mình.
Mục tiêu bài học
Sau bài này bạn có thể:
- Giải thích
SynchronizationContextlà gì và nó làm gì với continuation. - Vẽ lại chính xác chuỗi sự kiện dẫn tới deadlock
.Result. - Quyết định đúng nơi cần và không cần
ConfigureAwait(false). - Biết vì sao
ConfigureAwait(false)chỉ chữa triệu chứng, không chữa nguyên nhân.
Nội dung bài học
6.4.1 — SynchronizationContext là gì
Khi bạn await, runtime hỏi một câu: "phần còn lại của phương thức này nên chạy ở đâu?"
Câu trả lời mặc định là: ở nơi tôi vừa rời đi — tức SynchronizationContext.Current tại thời điểm await. Runtime chụp lại context đó và đăng ký continuation vào nó.
| Môi trường | SynchronizationContext.Current | Continuation chạy ở đâu |
|---|---|---|
| ASP.NET Framework (System.Web) | AspNetSynchronizationContext | Đúng thread request, một lúc một cái |
| WinForms | WindowsFormsSynchronizationContext | Đúng UI thread |
| WPF | DispatcherSynchronizationContext | Đúng UI thread |
| ASP.NET Core | null | Thread pool bất kỳ |
| Console | null | Thread pool bất kỳ |
Dòng in đậm là lý do cả bài này nhẹ đi rất nhiều với .NET hiện đại: ASP.NET Core đã bỏ hẳn SynchronizationContext.
6.4.2 — Deadlock xảy ra chính xác như thế nào
// ASP.NET Framework (KHÔNG phải Core)
public ActionResult GetData()
{
var data = GetDataAsync().Result; // (1) block thread request
return View(data);
}
private async Task<string> GetDataAsync()
{
return await _http.GetStringAsync("https://api.example.com"); // (2)
}
Chuỗi sự kiện:
.Resultblock thread request. Thread này đang giữAspNetSynchronizationContext.- HTTP trả về. Continuation của
GetDataAsyncđược đăng ký vào context đó. - Context chỉ cho phép một thao tác tại một thời điểm, và slot đó đang bị
.Resultchiếm. - Continuation không bao giờ chạy →
Taskkhông bao giờ hoàn thành →.Resultkhông bao giờ trả về.
Hai bên chờ nhau vĩnh viễn. Không có exception, không có log — request chỉ đơn giản treo cho tới khi timeout.
Bài Gọi .Result khi nào thì deadlock, khi nào thì không? đi sâu vào việc vì sao cùng một dòng code treo trong WPF/ASP.NET Framework nhưng chạy bình thường trong Console/ASP.NET Core.
6.4.3 — ConfigureAwait(false) phá vòng lặp ra sao
private async Task<string> GetDataAsync()
{
return await _http.GetStringAsync("https://api.example.com")
.ConfigureAwait(false); // không quay về context
}
Với ConfigureAwait(false), continuation chạy trên thread pool bất kỳ. Nó không cần cái slot đang bị .Result chiếm, nên nó chạy được, Task hoàn thành, .Result trả về.
Nhưng hãy nhìn cho đúng bản chất:
ConfigureAwait(false)không chữa nguyên nhân. Nguyên nhân là.Result.ConfigureAwait(false)chỉ làm cho một lỗi thiết kế tạm thời không phát nổ.
Cách chữa thật sự là async all the way: public async Task<ActionResult> GetData() => View(await GetDataAsync());
6.4.4 — N ó lan truyền theo từng await, không theo phương thức
Đây là điểm rất hay bị hiểu sai:
public async Task ProcessAsync()
{
await StepOneAsync().ConfigureAwait(false); // continuation: thread pool
await StepTwoAsync(); // continuation: context da mat roi
}
ConfigureAwait áp dụng cho đúng một await, không phải cả phương thức. Nhưng sau await đầu tiên với false, bạn đã ra khỏi context — nên await thứ hai dù không ghi gì cũng chẳng có context nào để quay về.
Hệ quả: trong thư viện, nếu đã dùng thì phải dùng nhất quán ở mọi await. Dùng nửa vời gây ra hành vi phụ thuộc vào việc thao tác nào hoàn thành đồng bộ — nghĩa là bug chỉ xuất hiện khi có cache hit, hoặc chỉ trên máy production.