1. Bí quyết cho mã đa luồng bền vững
Nếu tưởng tượng đa luồng như một đội vài thợ cùng sửa một chiếc xe đồng thời, thì rõ ràng: chỉ cần một thợ cầm nhầm dụng cụ — là công việc tắc nghẽn. Trong code cũng vậy: xử lý không cẩn thận với dữ liệu chung dẫn tới lỗi ẩn, chỉ hiện khi “ra trận”.
Trong bài này — cách viết mã đa luồng mà không sụp như nhà lá. Và còn: những công cụ nào cứu bạn khi có vấn đề.
1. Giảm tối thiểu các critical section (lock)
Càng ít code nằm trong khối lock càng tốt. Khi một thread giữ khoá, các thread khác chờ.
Ví dụ:
// TỒI: Toàn bộ business logic trong lock — mọi thread phải chờ
lock(_locker)
{
// Hoạt động lâu (không liên quan đến resource chung)
Thread.Sleep(500);
counter++;
}
// TỐT: Chỉ hành động tối thiểu trong lock
// công việc nặng ở ngoài lock
Thread.Sleep(500);
lock(_locker)
{
counter++;
}
Thực tế: Nếu trong lock có gọi network hoặc tính toán lâu, performance sẽ giảm mạnh.
2. Không dùng đối tượng chung làm khóa
Dùng lock(this) hoặc lock(typeof(MyClass)) — là ý tưởng tệ.
Tại sao? Nếu ai đó khác dùng cùng object đó để lock, bạn sẽ gặp deadlock hoặc bug ẩn. Luôn dùng một object private riêng:
private readonly object _locker = new object();
lock(_locker)
{
// Hành động của bạn
}
Cấm: string (string), public field, value-type objects.
3. Luôn dùng try...finally để giải phóng resource đã chiếm
Bất kỳ việc lấy Mutex, semaphore, ReaderWriterLockSlim — phải được release trong finally.
_mutex.WaitOne();
try
{
// Critical section
}
finally
{
_mutex.ReleaseMutex();
}
4. Đừng lạm dụng synchronization
Chỉ đồng bộ hoá truy cập tới resource chung thực sự (ví dụ collection), chứ không phải “mỗi khì”. Lock thừa biến code thành hàng đợi chờ.
5. Dùng collection và kiểu thread-safe
.NET cung cấp collections đặc biệt cho kịch bản đa luồng: ConcurrentDictionary, ConcurrentQueue, ConcurrentBag, BlockingCollection v.v. Chúng đã tự bảo vệ bên trong.
using System.Collections.Concurrent;
ConcurrentDictionary<int, string> users = new ConcurrentDictionary<int, string>();
users.TryAdd(1, "Vasya");
users[2] = "Petya";
6. Cẩn thận với deadlock (vòng khóa)
Cái bẫy điển hình — chiếm nhiều lock theo thứ tự khác nhau.
// Thread 1
lock(obj1)
{
lock(obj2)
{
// Làm gì đó
}
}
// Thread 2
lock(obj2)
{
lock(obj1)
{
// Làm gì đó
}
}
Lời khuyên: Luôn lấy lock theo cùng một thứ tự trên tất cả các thread.
7. Nếu có thể, dùng immutable state
Nếu object không thay đổi trạng thái sau khi tạo — nó an toàn để đọc từ mọi thread. Ví dụ: string, Tuple, DateTime, DTO chỉ-read.
2. Công cụ để chẩn đoán vấn đề đa luồng
Lỗi đồng bộ khó nhằn: hiếm và không đoán trước được. Dùng công cụ và phương pháp giúp bắt và phân tích chúng.
1. Logging sự kiện và thread
Log Thread.ManagedThreadId hiện tại và các thao tác mấu chốt — cách đơn giản để biết “ai và khi nào” vào/ra critical section.
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Đã vào critical section");
// ...
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Đã ra khỏi critical section");
Với app thực tế dùng Microsoft.Extensions.Logging, NLog, Serilog.
2. Thread Sanitizer & Race Detector
Trong .NET không có ThreadSanitizer “hoàn hảo” built-in, nhưng có công cụ hữu ích:
- JetBrains ReSharper — inspections bắt một số pattern nguy hiểm.
- Roslyn Analyzers — static code analysis.
- Concurrency Visualizer — phân tích wait/lock và load của threads.
3. Visual Studio Diagnostics Tools
Với profiler của Visual Studio bạn có thể thấy:
- những thread nào tồn tại trong app;
- nơi thread đang idle (waiting);
- nơi xảy ra lock và contention;
- khi nào xuất hiện deadlock và contention.
Chụp traces để có đồ thị chi tiết về sử dụng lock.
4. Phân tích dump và WinDbg
Nếu server “treo”, chụp dump process và mở bằng WinDbg hoặc dotnet-dump. Qua stack trace thấy thread bị kẹt ở đâu và ai đang giữ lock nào.
Ví dụ phân tích stack:
0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
1 000001d4b6f90e08 1 1 000001d4b5c941c0 000001d4b6f03458
(Dump thường là công cụ của những “jadoi deploy” có kinh nghiệm — đừng sợ, đó là công cụ mạnh.)
5. Unit-test với stress (stress testing)
Chạy mã đa luồng song song hàng trăm/nghìn lần — các race hiếm sẽ dễ được tìm hơn.
[Test]
public void Counter_IsThreadSafe()
{
var counter = 0;
var locker = new object();
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tasks.Add(Task.Run(() =>
{
for (int j = 0; j < 10000; j++)
{
lock (locker)
{
counter++;
}
}
}));
}
Task.WaitAll(tasks.ToArray());
Assert.AreEqual(100 * 10000, counter);
}
6. Dùng asserts và checks đặc biệt
Thêm kiểm tra đảm bảo trạng thái đúng trong debug. Ví dụ, Debug.Assert khi cố gắng tái chiếm resource bởi cùng một thread.
3. Kết luận và khuyến nghị
Sơ đồ trực quan: vùng nguy hiểm và an toàn
graph TD
A[Tài nguyên chung] -- không đồng bộ --> B(Trạng thái race)
A -- khóa (lock/Mutex) --> C[Truy cập an toàn: critical section]
C -- "quá nhiều lock" --> D(Mất hiệu năng)
A -- ReaderWriterLockSlim --> E{Nhiều reader / Một writer}
E -- "Đọc" --> F[Nhiều thread đọc cùng lúc]
E -- "Ghi" --> G[Chỉ một ghi, các thread khác chờ]
Synchronization primitives và mục đích của chúng
| Primitive | Dùng để làm gì | Số thread được cho phép | Liên tiến trình | Hiệu năng | Nên dùng ở đâu |
|---|---|---|---|---|---|
|
Critical section đơn giản | 1 | Không | Rất cao | 99% các case |
|
Tương tự nhưng giữa các process | 1 | Có | Trung bình | Files, IPC |
|
Tối đa N thread | N | Có | Trung bình | Resource pools |
|
Tương tự nhưng nhanh hơn, trong process | N | Không | Cao | Pool trong code |
|
Nhiều reader, một writer | Nhiều/1 | Không | Cao | Cache, settings |
GO TO FULL VERSION