1. Giới thiệu
Gọi chặn (blocking call) — là bất kỳ cuộc gọi nào "chặn" luồng thực thi cho tới khi một thao tác hoàn thành. Thường là:
- Đọc hoặc ghi dữ liệu (ví dụ, từ đĩa).
- Yêu cầu lâu tới cơ sở dữ liệu hoặc mạng.
- Gọi thư viện bên ngoài mà chạy "chậm".
Trong những lúc đó luồng chỉ đứng im, chân co chân duỗi và chờ: "Sao rồi, bao giờ có trả lời?"
Ví dụ đời thường
// Vlogic chính của bạn
Console.WriteLine("Đang chờ phản hồi từ server...");
string response = CallServer(); // ở đây mọi thứ bị treo!
Console.WriteLine("Server trả lời: " + response);
Cho tới khi server trả lời, luồng của bạn "bị treo" — và không thể làm gì khác!
Trông nó như thế nào trong ứng dụng
Trong các loại ứng dụng khác nhau hiện tượng này xuất hiện khác nhau. Trong chương trình console trông cứ như nó "đứng im" — con trỏ ngừng nháy và không thấy phản hồi. Trong GUI, ví dụ WinForms hoặc WPF, cửa sổ đột nhiên không phản hồi, xuất hiện "not responding", và người dùng sẵn sàng nguyền rủa phần mềm và cả họ tên của bạn. Trên server thì tệ hơn: một luồng bị treo có nghĩa là giảm một người dùng được phục vụ.
2. Gọi chặn trong C# thực tế
Hãy "sờ" thử tình huống bằng tay, dùng dự án học. Giả sử ta có ứng dụng quả cầu ma thuật. Ví dụ như đoạn sau:
Console.WriteLine("Nhập câu hỏi của bạn cho quả cầu ma thuật:");
string question = Console.ReadLine(); // gọi chặn: chờ nhập!
Console.WriteLine("Đang suy nghĩ về câu trả lời...");
Thread.Sleep(5000); // Mô phỏng công việc lâu
Console.WriteLine("Trả lời: thử lại vào ngày mai!");
- Console.ReadLine() — chờ nhập người dùng bao lâu cần thì chặn luồng.
- Thread.Sleep(5000) — tạo tạm dừng nhân tạo, đóng băng luồng.
Câu hỏi: Có gì xấu nếu chúng ta chỉ làm việc với ứng dụng console?
Trả lời: Với console thì không nghiêm trọng lắm — người dùng tự chờ. Nhưng nếu người dùng không chỉ một? Nếu đó là server nhận cùng lúc 1000 request? Hoặc ứng dụng GUI nơi người dùng cần phản hồi của giao diện, chứ không phải triết lý của bạn?
Ví dụ: Khóa UI
// Trong WinForms handler nút:
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "Đang tải...";
DoHeavyWork(); // thao tác nặng (ví dụ, truy vấn DB)
label1.Text = "Xong!";
}
Vấn đề: Trong khi DoHeavyWork chạy, cửa sổ không cập nhật giao diện, không phản hồi phím và chuột, không vẽ lại. Nếu người dùng cố đóng cửa sổ — Windows sẽ hiện "not responding" và đề nghị kết thúc tiến trình.
3. Nỗi đau lớn của thế giới đa luồng
Vấn đề của các gọi chặn
- Lãng phí tài nguyên. Mỗi thread (Thread) trong .NET là đối tượng khá "nặng". OS cấp bộ nhớ cho stack (thường 1 MB), descriptor, sync v.v. Nếu thread "nhàn rỗi" (chờ file hoặc mạng) thì nó vẫn tiêu tốn tài nguyên ("không làm gì nhưng ăn RAM").
- Giới hạn về số luồng. Trên server thường có pool luồng. Nếu tất cả luồng bị chặn — request mới không được xử lý. Ví dụ trong ASP.NET: nếu mọi thread đang chờ DB, trang web "treo" với người dùng mới.
- Giao diện phản hồi kém. Trong ứng dụng GUI, cửa sổ ngừng phản hồi ngay cả với hành động "đóng" khi UI-thread bị chặn.
- Sụt hiệu năng. Càng nhiều thread bị chặn, càng nhiều context switch, tải lên OS, khiến toàn bộ máy chạy chậm hơn.
Nguồn phổ biến của gọi chặn
Xem qua những cuộc gọi có thể "vô tình" chặn luồng:
- Thao tác mạng: gọi API, tải file, download update.
- Đĩa/hệ thống file: đọc hoặc ghi file lớn.
- Cơ sở dữ liệu: query lâu tới SQL Server hay DB cục bộ.
- Nhập người dùng: ReadLine lâu — nhỏ nhưng vẫn chặn.
- Thread.Sleep, Task.Delay: "đóng băng" luồng nhân tạo.
Ví dụ (tải dữ liệu từ web)
using System.Net.Http;
HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // gọi đồng bộ chặn!
Console.WriteLine(result);
Chuyện gì xảy ra ở đây? Phương thức .Result chặn luồng cho tới khi có phản hồi!
4. Làm sao biết cuộc gọi của bạn có chặn luồng?
Đơn giản: nếu phương thức không trả quyền điều khiển cho tới khi xong việc (chờ, nhập, mạng, đĩa) — đó là gọi chặn.
- Phương thức có Thread.Sleep, Task.Wait, .Result, .Wait() — chặn.
- Phương thức đọc/ghi dữ liệu mà không dùng async — cũng chặn.
- Cửa sổ treo hay server bị khóa — dấu hiệu thường gặp.
Yếu tố trực quan: Sơ đồ khối gọi chặn
+-------------------+
| Bắt đầu gọi hàm |
+-------------------+
|
v
+-------------------+
| Gọi hàm chặn |
| (ví dụ đọc file) |
+-------------------+
|
v
+-------------------+
| Chờ thao tác hoàn |
| thành |
+-------------------+
|
v
+-------------------+
| Trả về khỏi hàm |
| và tiếp tục |
+-------------------+
5. Tại sao gọi chặn gây hại trên server
Phần lớn ứng dụng hiện nay là mạng hoặc server. Ngay cả game cũng tải từ server. Nếu làm website thì chắc chắn có xử lý mạng và đĩa.
Giả sử server bạn xử lý 1000 request mỗi giây. Mỗi request cần truy vấn DB. Bạn viết:
// Web-handler
string data = db.ReadDataSync(); // Đồng bộ chặn!
return new Response(data);
Trong khi query DB diễn ra, thread chỉ ngồi chờ. Một request thì chưa sao. Nhưng khi nhiều request cùng lúc, tất cả thread sẽ đầy. Request mới không có thread để xử lý và bị xếp hàng. Cuối cùng server chẳng khác gì một hàng dài nơi mọi người đứng đợi và ngáp, chờ tới lượt.
Minh họa: "Xử lý request đồng bộ"
Request 1: chiếm thread -> chờ DB -> giải phóng
Request 2: chiếm thread -> chờ DB -> giải phóng
...
Request 100: không còn thread rảnh, phải chờ hàng đợi...
6. Bất đồng bộ — thuốc trị gọi chặn
Để đánh bại chặn ta dùng gọi bất đồng bộ. Trong C# đó là từ khóa: async và await.
Chúng cho phép:
- Không chặn luồng (đặc biệt là UI- hoặc server-thread quan trọng).
- Không chiếm tài nguyên vô nghĩa.
- Cải thiện phản hồi của GUI.
- Không "giữ" luồng trong thời gian thao tác chậm diễn ra.
Lưu ý: không phải lúc nào cũng bắt đầu với async ngay lập tức, vì nó đòi hỏi hiểu về luồng và chặn. Khi bạn đã thấy nỗi khổ vì gọi chặn, cách tiếp cận bất đồng bộ sẽ rất đã cho cả bộ não và người dùng.
Bảng: Cái gì chặn, cái gì không?
| Phương thức/Gọi | Chặn luồng? | Phù hợp cho UI/Server? |
|---|---|---|
|
Có | Không |
|
Có | Không |
|
Có | Không |
|
Có | Không |
|
Có | Không |
|
Không | Có |
|
Không | Có |
|
Không | Có |
NB: await chỉ hoạt động trong hàm bất đồng bộ (async Task ...), chúng ta sẽ xem kỹ trong các bài sau.
7. Những lỗi kinh điển khi làm việc với gọi chặn
"Khóa UI — người dùng nguyền rủa mọi thứ"
private void btnLoad_Click(object sender, EventArgs e)
{
var data = BigFileReader.Read(@"C:\huge.dat"); // gọi chặn!
textBox1.Text = data;
}
Cho tới khi file khổng lồ đọc xong — cửa sổ "bị treo".
"Server treo — client chạy mất"
public IActionResult Download()
{
var content = File.ReadAllBytes("bigfile.zip"); // đồng bộ, lâu, chặn thread ASP.NET!
return File(content, "application/zip");
}
Server nhỏ (ví dụ host miễn phí) có thể đơn giản "chết" với cách này.
"Phương thức async + .Wait()/.Result = cái chết đồng bộ"
public void LoadData()
{
var result = DoAsyncWork().Result; // Chặn thread!
}
.Result và .Wait() biến mã bất đồng bộ đẹp đẽ thành cuộc gọi chặn nhàm chán.
GO TO FULL VERSION