1. Giới thiệu
Trong lập trình, người mới (và thậm chí các dev có kinh nghiệm) thường nhầm lẫn giữa hai khái niệm tương tự nhưng thực chất khác nhau: multithreading và asynchronous. Ở phỏng vấn câu hỏi này hay được hỏi để xem người ta có hiểu được sự khác biệt không, vì điều đó quyết định trực tiếp cách viết mã nhanh và phản hồi tốt.
Hãy cùng tìm hiểu xem mấu chốt là gì.
Multithreading: khi nhiều tay cùng làm
Multithreading — là cách tổ chức chương trình bằng nhiều thread. Thread là một luồng thực thi, một "làn đường" riêng mà CPU chạy các câu lệnh của chương trình. Một process (ví dụ ứng dụng .NET của chúng ta) có thể khởi chạy nhiều thread để các nhiệm vụ khác nhau chạy cùng lúc.
Ví dụ đời thực: Bạn là project manager, giao các nhiệm vụ khác nhau cho đồng nghiệp, và tất cả những nhiệm vụ đó được làm cùng lúc. Ví dụ, một người viết báo cáo, người kia gọi khách hàng, người thứ ba làm slide.
Ý tưởng chính của multithreading: các nhiệm vụ thực sự diễn ra song song (hoặc quasi-parallel nếu chỉ có một CPU, nhờ context switching nhanh).
Asynchronous: biết cách đừng để thời gian trôi lãng phí
Asynchronous — là cách tổ chức mã để chương trình có thể làm việc khác trong khi chờ một thao tác tốn thời gian (ví dụ, phản hồi từ Internet hoặc đọc file). Async code không nhất thiết phải dùng nhiều thread! Nó chỉ không block luồng của bạn trong khi chờ một thao tác hoàn thành.
Ví dụ đời thực: Thay vì đứng đợi uống cà phê từ máy pha, bạn giao việc cho máy và làm việc khác (trả email, đọc tin), rồi lấy cà phê khi nó xong.
Ý tưởng chính của asynchronous: không để thời gian chờ các tác vụ "dài" bị lãng phí, mà làm việc có ích khác.
2. Khác nhau ở chỗ nào?
Rất thường asynchronous và multithreading được dùng cùng nhau, và điều đó làm rối hơn. Nhưng thực ra hai kỹ thuật trả lời những câu hỏi khác nhau:
- Multithreading cần để thực sự paralell công việc, tận dụng CPU/cores có sẵn.
- Asynchronous cần để thông minh tổ chức việc chờ tài nguyên (network I/O, disk I/O...), không block thread.
Có thể kết hợp chúng, nhưng chúng không bắt buộc phải liên quan trực tiếp với nhau.
Mô phỏng ngắn
| Multithreading (Threads) | Asynchronous (Async) | |
|---|---|---|
| Khi nào dùng? | Khi nhiệm vụ "ăn" CPU (CPU-bound: tính toán, render, xử lý mảng) | Khi nhiệm vụ "chờ" sự kiện (I/O-bound: mạng, ổ đĩa, DB) |
| Mã làm gì? | Gây tải cho CPU, khởi chạy nhiều thread, thực sự song song | Khi chờ dữ liệu ("idle"), giải phóng thread — thread đó có thể làm việc khác |
| Quản lý cái gì? | Số lượng thread thực thi đồng thời | Ai đang bận/ai rảnh, và làm gì khi thao tác hoàn thành |
| Ví dụ điển hình | Nén video, render ảnh | Tải file, HTTP request tới server |
Ví dụ 1: Multithreading — tính nhanh
Giả sử ta có nhiệm vụ nặng: tính tổng của các số lớn.
void ComputeSum(long start, long end)
{
long sum = 0;
for (long i = start; i <= end; i++)
{
sum += i;
}
Console.WriteLine($"Tổng từ {start} đến {end} = {sum}");
}
// Khởi chạy ba nhiệm vụ cùng lúc — mỗi cái tính phần của nó
Thread t1 = new Thread(() => ComputeSum(1, 1000_000_000));
Thread t2 = new Thread(() => ComputeSum(1000_000_001, 2000_000_000));
Thread t3 = new Thread(() => ComputeSum(2000_000_001, 3000_000_000));
t1.Start();
t2.Start();
t3.Start();
// Chờ tất cả thread hoàn thành
t1.Join();
t2.Join();
t3.Join();
Tại sao dùng thread ở đây?
Vì đây là công việc CPU-bound. Threads thật sự tải CPU, và nếu máy bạn nhiều nhân — công việc sẽ nhanh hơn.
Ví dụ 2: Asynchronous — chờ phản hồi từ server
Mạng là thứ chậm, và khi chờ phản hồi từ server, thread có thể "rảnh".
// Tải trang web bất đồng bộ, thread không bị block
async Task DownloadPageAsync()
{
using HttpClient client = new HttpClient();
string html = await client.GetStringAsync("https://dotnet.microsoft.com/");
Console.WriteLine(html.Length);
}
Tại sao dùng async ở đây?
Chúng ta ra lệnh "Bắt đầu tải", và không block thread, mà chờ thông báo khi dữ liệu về.
3. Asynchronous không cần multithreading: thật hay giả?
Câu hỏi: Có phải mọi async code đều khởi tạo thread mới?
Trả lời: Không! Thường async không cần thêm thread.
Khi bạn làm ví dụ await file.ReadAsync(...), .NET khởi chạy thao tác bất đồng bộ ở tầng OS, và thread đã gọi ngay lập tức trở nên "rảnh" và trả về thread pool. Khi thao tác hoàn tất, một thread rảnh từ pool sẽ tiếp tục thực hiện phần còn lại của nhiệm vụ.
- Nếu thay async bằng đồng bộ (file.Read(...)) — thread sẽ chờ hoàn thành thao tác và không làm gì khác.
- Async code nói: "CPU, trong khi chờ — làm việc khác đi!"
Minh hoạ quan trọng:
// "Thread" không bị block, chỉ chờ thao tác sẵn sàng
await Task.Delay(1000); // Chỉ đợi 1 giây — không tốn CPU!
Multithreading không có async
Có những trường hợp chỉ cần paralell bằng threads: tính toán nặng, vòng lặp lớn xử lý data... Trong trường hợp này async vô ích để tăng tốc chính các tính toán, vì CPU đã bị tải 100%.
Ví dụ cổ điển: xử lý file lớn
// Code này thật sự tải CPU — async không giúp được.
void CalculateHash(string file)
{
byte[] data = File.ReadAllBytes(file); // đồng bộ!
// Tính hash...
}
Muốn nhanh hơn — chạy nhiều thread, mỗi thread xử lý file riêng của nó.
4. Trông thế nào trong app của bạn?
Thao tác bất đồng bộ (await)
Trong app học của chúng ta có thể thêm tải dữ liệu bất đồng bộ. Ví dụ, bạn yêu cầu tỷ giá tiền tệ hoặc thời tiết — tốt hơn là làm async.
async Task GetWeatherAsync(string city)
{
using HttpClient client = new HttpClient();
string json = await client.GetStringAsync($"https://api.weather.com/{city}");
// Tiếp tục khi phản hồi tới
Console.WriteLine($"Thời tiết ở {city}: {json}");
}
Điều gì xảy ra phía sau?
Lệnh await "cắt" method của bạn thành hai phần:
- Gọi thao tác async — thread OS "được giải phóng" và có thể làm việc khác.
- Khi dữ liệu về — thực thi method tiếp tục trên một thread rảnh từ pool.
Multithreading cho các tính toán phức tạp
Trong ví dụ máy tính của chúng ta (giả sử nó xử lý mảng lớn) — đáng để chạy tính toán trên các thread riêng.
// Chia nhiệm vụ lớn thành các phần nhỏ, mỗi phần do một thread tính
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 4; i++)
{
int rangeStart = i * 1000000;
int rangeEnd = (i + 1) * 1000000 - 1;
Thread t = new Thread(() => ComputeSum(rangeStart, rangeEnd));
threads.Add(t);
t.Start();
}
// Chờ tất cả thread kết thúc
foreach (Thread t in threads) t.Join();
5. Lỗi thường gặp và lưu ý khi làm việc với async
Lỗi #1: dùng async "để nhanh".
Một hiểu lầm phổ biến là nghĩ async làm mã chạy nhanh hơn. Không phải vậy. Asynchronous — về responsiveness, chứ không phải tốc độ.
Nếu nhiệm vụ là CPU-bound — async sẽ không làm nó nhanh hơn.
Nếu nhiệm vụ I/O-bound (mạng, đĩa) — async hữu ích vì thread không bị idle vô ích và có thể làm việc khác.
Lỗi #2: block thread bằng Wait() và Result.
Trong async code tuyệt đối không nên gọi Wait() hoặc property Result trên Task. Điều này hầu như luôn dẫn đến block thread và deadlock.
// Tệ! Sẽ block thread và gây vấn đề
var result = GetDataAsync().Result;
async Task<string> GetDataAsync() { /* ... */ return "data"; }
Cách đúng — dùng await và không block thread.
Lỗi #3: async và UI.
Trong ứng dụng GUI (WPF, WinForms) vấn đề chính là không để UI thread bị treo. Nếu chạy thao tác dài hoặc blocking trên main thread, toàn bộ app sẽ "đơ". Async giải quyết bằng cách chạy công việc nặng ở background, còn UI vẫn phản hồi.
Lỗi #4: thiếu quy ước đặt tên cho method bất đồng bộ.
Nếu không thêm hậu tố Async cho các method bất đồng bộ, dễ nhầm lẫn method nào đồng bộ, method nào async. Điều này dẫn tới block vô ý và lỗi khi gọi. Luôn đặt tên các method async có hậu tố Async.
GO TO FULL VERSION