1. Giới thiệu
Livelock — là tình huống trong ứng dụng đa luồng khi hai hoặc nhiều thread cố tránh deadlock (Deadlock), nhưng cuối cùng chỉ phản ứng liên tục với hành động của nhau mà không tiến lên được. Không giống Deadlock, nơi các thread đóng băng và không thể làm gì, với Livelock chương trình trông vẫn "sống": các thread chạy, nhưng không ai làm việc hữu ích.
Nó giống như bạn và đồng nghiệp trong một hành lang hẹp liên tục lùi bước để nhường nhau và vì vậy không ai đi qua được. Vui nhìn từ bên ngoài, nhưng thảm họa trong phần mềm.
Ví dụ Livelock: lập trình viên ở cửa
Hãy tưởng tượng hai lập trình viên rất lịch sự đi đối diện nhau trong hành lang hẹp. Họ cùng lúc đến cửa và đều quyết định nhường: lùi sang một bên để người kia đi trước. Cả hai thấy tình hình chưa thay đổi, lại nhường tiếp — vô tận. Không ai đứng yên, mọi người đều di chuyển, nhưng chẳng ai đi qua cửa.
Trong chương trình đa luồng điều này trông như: một thread thấy resource đang bận, lùi lại, kiểm tra lại, lại nhường, và cứ thế mãi.
2. Livelock trong thực tế
Hãy triển khai tình huống tương tự trong code. Giả sử ta có hai resource (ví dụ hai tài khoản ngân hàng), và hai thread cùng lúc cố chuyển tiền cho nhau, cố gắng không "kẹt cứng".
class Account
{
public int Balance { get; set; }
public object LockObj { get; } = new object();
}
public class Program
{
static void Transfer(Account from, Account to, int amount)
{
while (true)
{
bool lockedFrom = false;
bool lockedTo = false;
try
{
Monitor.TryEnter(from.LockObj, 100, ref lockedFrom);
Monitor.TryEnter(to.LockObj, 100, ref lockedTo);
if (lockedFrom && lockedTo)
{
from.Balance -= amount;
to.Balance += amount;
break; // Chuyển xong!
}
}
finally
{
if (lockedFrom) Monitor.Exit(from.LockObj);
if (lockedTo) Monitor.Exit(to.LockObj);
}
// Không được? Lùi lại rồi thử lại — lịch sự!
Thread.Sleep(1);
}
}
// ... Khởi chạy hai thread với Transfer(A, B, ...); Transfer(B, A, ...);
}
Trong ví dụ này cả hai thread liên tục cố lấy locks. Nhưng nếu cả hai luôn thấy locks đang bận và đều nhường, thì mọi thứ chỉ thành "nhường nhau" lịch sự và không có chuyển khoản nào xảy ra. Tin vào tính ngẫu nhiên của scheduler là chiến lược tệ cho chương trình đáng tin cậy. Hãy thêm khoảng dừng và backoff, đừng để vòng lặp rỗng ngắn gọi Monitor.TryEnter và Thread.Sleep liên tục.
So sánh Livelock vs Deadlock
| Deadlock | Livelock | |
|---|---|---|
| Threads | Đứng yên, chờ | Chạy tích cực, nhưng chờ |
| Resources | Bị khóa vĩnh viễn | Rõ ràng không bị khóa |
| CPU | Gần như không tải | Có thể đầy 100% |
| Giải pháp | Timeouts, thứ tự khóa | Ngẫu nhiên, dừng, backoff |
3. Starvation (đói): khi hàng đợi không tới lượt
Starvation (nghĩa đen là "đói") — là tình huống khi một hoặc vài thread liên tục bị bỏ qua và không có quyền truy cập vào resource cần thiết, vì các thread khác luôn luôn đứng trước.
Không giống Deadlock và Livelock, ở đây không ai bị khóa vĩnh viễn và không ai mãi mãi nhường — chỉ có vài thread mãi không được "miếng bánh".
Minh họa: tương tự trong thực tế
Hãy tưởng tượng căng tin với hai hàng: "VIP" (ví dụ quầy cho nhân viên) và "cho tất cả". Hàng VIP ngắn hơn nhưng luôn có người kịp chen trước. Khách bình thường đứng nửa tiếng nhìn VIP lại đi trước. Đó chính là Starvation!
4. Starvation trong C#: thực hành và gỡ lỗi
Xem xét tình huống với lock, khi một thread có priority cao liên tục vào được critical section, còn thread khác "tụt lại":
private static readonly object _locker = new object();
static void Greedy()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("Luồng tham đã chiếm resource...");
Thread.Sleep(10); // giữ lock lâu hơn
}
Thread.Sleep(1);
}
}
static void Poor()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("Luồng khổ đã thử vào...");
}
}
}
Nếu chạy cả hai thread, "Greedy" (tham) giữ lock lâu và nhanh chóng chiếm lại, còn "Poor" (khổ) hầu như không vào được bên trong và thực sự "đói" resource. Trong thực tế starvation có thể ít rõ ràng hơn: ví dụ thread có priority thấp hơn hoặc thường bị OS preempt.
Nơi thường gặp starvation?
- Trong các thread có priority thấp.
- Khi tổ chức hàng đợi nhiệm vụ sai (không có chính sách FIFO công bằng).
- Trong ReaderWriterLockSlim với cài đặt mặc định: nếu writer đến ít và readers liên tục, writer sẽ luôn bị "đói", vì readers chiếm Read Lock và không cho Write Lock.
5. Tại sao Starvation — không phải lúc nào cũng bug, nhưng luôn là vấn đề
Starvation, không giống Deadlock, không làm chương trình dừng hẳn, nhưng kết quả hoạt động trở nên không đoán trước và không công bằng: dữ liệu xử lý không đồng đều, một số tác vụ chờ mãi, hiệu suất giảm. Điều này đặc biệt nghiêm trọng trong ứng dụng server, nơi mọi thứ cần cơ hội công bằng.
Cách phát hiện và chẩn đoán Livelock và Starvation
- CPU tăng mạnh, nhưng task "đứng yên" — có thể bạn đang gặp Livelock.
- Các thread có vẻ đang chạy, nhưng một vài task chẳng bao giờ kết thúc — có khả năng Starvation.
- Logs: nếu nhật ký cho thấy một số thread hoặc task không bao giờ được truy cập resource — chắc chắn là "đói".
Cách tránh Livelock
- Đừng chỉ dựa vào các phương pháp non-blocking "nhường nhịn" như Monitor.TryEnter. Nếu thất bại, thêm các khoảng dừng ngẫu nhiên (Thread.Sleep(Random.Next(1, 10)))), để các thread không đồng bộ thời điểm cố gắng.
- Đừng dùng vòng lặp ngắn liên tục để cố chiếm resource — nếu không vòng "lùi — thử" sẽ vô tận.
- Đôi khi thay đổi thuật toán giúp: đặt "quyền ưu tiên cho người lớn tuổi", để chỉ một thread được nhường, thread kia không.
Cách tránh Starvation
- Quản lý priority của thread: background hoặc task ưu tiên không nên luôn chặn những task khác.
- Dùng hàng đợi (ConcurrentQueue<T>, channels, task queue) với chính sách FIFO công bằng, để task được phục vụ theo thứ tự.
- Trong ReaderWriterLockSlim có thể tạm dừng reader mới để ưu tiên writer, giúp writer chờ có thể lấy Write Lock.
- Dùng timeouts và logs: nếu thread cố lấy lock quá lâu, log cảnh báo.
- Giảm thời gian giữ khóa và rút ngắn critical section.
So sánh Deadlock, Livelock và Starvation
| Tình huống | Deadlock | Livelock | Starvation |
|---|---|---|---|
| Threads | Đứng chết | Chạy nhưng vô ích | Một vài làm việc, vài cái không |
| Truy cập resource | Không | Không | Không đồng đều, thỉnh thoảng không |
| CPU | Không tải | Tải cao | Có tải, nhưng không phải tất cả |
| Lỗi nghiêm trọng? | Có | Có | Đôi khi |
GO TO FULL VERSION