CodeGym /Các khóa học /C# SELF /Khóa chéo: Deadlocks

Khóa chéo: Deadlocks

C# SELF
Mức độ , Bài học
Có sẵn

1. Ví dụ Deadlock trên C#

Deadlock — là tình huống khi mỗi luồng trong một nhóm chiếm giữ một resource và chờ resource khác được giải phóng, mà resource đó đang bị chiếm bởi luồng khác trong cùng nhóm — và không ai bao giờ nhận được thứ họ chờ.

Ta xem ví dụ thực tế. Giả sử có hai object làm khóa và hai luồng. Mỗi luồng khóa một object rồi cố gắng lấy khóa thứ hai.

using System;
using System.Threading;

class Program
{
    static readonly object lockerA = new object();
    static readonly object lockerB = new object();

    static void Main()
    {
        Thread thread1 = new Thread(Thread1Work);
        Thread thread2 = new Thread(Thread2Work);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Cả hai luồng đã kết thúc (nếu không xảy ra Deadlock)");
    }

    static void Thread1Work()
    {
        lock (lockerA)
        {
            Console.WriteLine("Luồng 1: đã chiếm lockerA");
            Thread.Sleep(100); // Cho luồng 2 có cơ hội chiếm lockerB

            lock (lockerB)
            {
                Console.WriteLine("Luồng 1: đã chiếm lockerB");
            }
        }
    }

    static void Thread2Work()
    {
        lock (lockerB)
        {
            Console.WriteLine("Luồng 2: đã chiếm lockerB");
            Thread.Sleep(100); // Cho luồng 1 có cơ hội chiếm lockerA

            lock (lockerA)
            {
                Console.WriteLine("Luồng 2: đã chiếm lockerA");
            }
        }
    }
}

Chạy mã này vài lần — rất có khả năng bạn sẽ gặp ứng dụng “treo”. Deadlock đã xảy ra! Mỗi luồng giữ một lock và chờ lock của luồng kia.

Minh hoạ Deadlock

Đây là sơ đồ nhìn như thế nào:

sequenceDiagram
    participant Luồng_1
    participant Luồng_2
    participant lockerA
    participant lockerB

    Luồng_1->>lockerA: chiếm lockerA
    Luồng_2->>lockerB: chiếm lockerB
    Luồng_1->>lockerB: chờ lockerB được giải phóng
    Luồng_2->>lockerA: chờ lockerA được giải phóng

Kết quả: Luồng 1 giữ lockerA và chờ lockerB, Luồng 2 giữ lockerB và chờ lockerA. Không ai chịu nhường — chương trình treo.

2. Làm sao phát hiện Deadlock?

Tại sao nó xảy ra?

Để hiểu nguyên nhân, nhìn vào vài thành phần:

  • Khóa nhiều cái cùng lúc: Khi một luồng chiếm nhiều lock cùng lúc.
  • Thứ tự khóa không nhất quán: Nếu các luồng khóa theo thứ tự khác nhau, deadlock có thể xảy ra.
  • Có khả năng chờ: Luồng chờ resource đang bị chiếm bởi luồng khác.
  • Không có ép buộc chiếm: Một luồng không thể “cướp” lock từ luồng khác trong C# — chỉ có thể chờ.

Bẫy cổ điển: nếu bạn có N luồng và N resource, và các luồng khóa theo thứ tự khác nhau — bạn đang đứng trước nguy cơ deadlock.

Dấu hiệu điển hình của Deadlock

Deadlock thường trông như một “treo” bình thường từ bên ngoài. Các luồng còn sống nhưng chờ nhau vô hạn.

Dấu hiệu thường gặp:

  • Ứng dụng đột ngột không phản hồi (đôi khi chỉ khi tải cao).
  • Debugger cho thấy các luồng dừng ở lock, Monitor.Enter, WaitOne, EnterReadLock hoặc các thao tác đồng bộ tương tự.
  • Công cụ hệ thống (Task Manager hoặc Rider/Visual Studio) hiển thị 0% CPU — “mọi thứ đứng im”.
  • Log không có lỗi, nhưng không có hoạt động nào.

Nếu dùng JetBrains Rider hoặc Visual Studio, kiểm tra debugger để xem luồng dừng ở đâu (Stack Trace). Nếu thấy nhiều luồng đang đứng chờ nhau trên lock/Mutex.WaitOne — chúc mừng (hoặc xin chia buồn): đó là deadlock!

Tình huống cổ điển gây Deadlock

Thứ tự khóa không đồng bộ. Như ví dụ ở trên: Luồng 1 khóa lockerA rồi lockerB; Luồng 2 ngược lại. Bẫy sẵn sàng.

Khóa lồng nhau. Khi trong một block lock bạn lại mở một lock khác trên object khác.

Resource giao nhau trong nhiều method. Khó theo dõi hơn nếu lock rải rác trong nhiều method/class và kết hợp khác nhau. Khi số lock > 2, kịch bản phức tạp hơn.

3. Ngăn ngừa Deadlock

Tin tốt: có thể phòng deadlock!
Tin xấu: cần cẩn thận khi thiết kế code đa luồng.

Vài lời khuyên “thực tế” (ghi nhớ nhé — phỏng vấn sẽ thích điều này!):

Luôn chiếm lock theo cùng thứ tự

Nếu có nhiều object có thể bị lock cùng nhau — xác định một thứ tự chung (ví dụ theo tên hoặc theo index) và luôn tuân theo.

Ví dụ — chiếm lock đúng thứ tự

static void SafeLock(object objA, object objB)
{
    // So sánh hashcode — để các luồng luôn khóa object theo cùng một thứ tự
    object first = objA.GetHashCode() < objB.GetHashCode() ? objA : objB;
    object second = objA.GetHashCode() < objB.GetHashCode() ? objB : objA;

    lock (first)
    {
        lock (second)
        {
            // critical section
        }
    }
}

Ở đây — cả hai luồng sẽ vào trước lock(objA) rồi lock(objB) nếu objA “nhỏ hơn” objB.

Sử dụng timeout

Nếu luồng không thể lấy lock trong thời gian hợp lý — nó nên ném exception hoặc thoát gọn. Dùng Monitor.TryEnter rất hợp lý.

Ví dụ với Monitor.TryEnter

bool lockTakenA = false;
bool lockTakenB = false;

try
{
    Monitor.TryEnter(lockerA, 500, ref lockTakenA);
    if (!lockTakenA)
    {
        // Không lấy được lock trong 500 ms — bỏ cuộc để tránh deadlock
        return;
    }

    Monitor.TryEnter(lockerB, 500, ref lockTakenB);
    if (!lockTakenB)
    {
        return;
    }

    // Critical section...

}
finally
{
    if (lockTakenB)
        Monitor.Exit(lockerB);
    if (lockTakenA)
        Monitor.Exit(lockerA);
}

Nếu không lấy được lock — đừng đứng yên chờ!

Giảm tối thiểu phần khóa

Trong critical section chỉ giữ phần code tối thiểu cần thiết.

Đừng trộn lẫn nhiều cơ chế đồng bộ

Nếu cùng lúc dùng lock, Mutex, Semaphore, ReaderWriterLockSlim — nguy cơ rối và deadlock tăng lên.

4. Deadlock với Mutex, Semaphore và ReaderWriterLockSlim

Deadlock xảy ra không chỉ với lock hoặc Monitor, mà cả với các cơ chế đồng bộ khác.

Deadlock với Mutex

static Mutex mutexA = new Mutex();
static Mutex mutexB = new Mutex();

void Work1()
{
    mutexA.WaitOne();
    Thread.Sleep(100);
    mutexB.WaitOne();

    // Critical section

    mutexB.ReleaseMutex();
    mutexA.ReleaseMutex();
}

Nếu có luồng thứ hai khóa theo thứ tự ngược lại — bạn sẽ có deadlock.

Deadlock với ReaderWriterLockSlim

ReaderWriterLockSlim, dù linh hoạt, không tự động bảo vệ khỏi deadlock nếu một luồng đã có một kiểu lock rồi cố lấy kiểu khác.

Ví dụ, nếu luồng giữ read-lock (EnterReadLock) rồi cố lấy write-lock (EnterWriteLock) — deadlock rất dễ xảy ra, vì write-lock không thể được cấp khi vẫn còn read-lock.

5. Tránh Deadlock trong ứng dụng thực tế

Xem ví dụ mô phỏng ứng dụng cửa hàng online, nơi chạy đồng thời:

  • Luồng cập nhật tồn kho (writers)
  • Luồng kiểm tra tồn kho (readers)
  • Admin (luồng đặc biệt) vừa đọc vừa ghi

Tệ:

lock (stockLock)
{
    // cập nhật kho
    lock (userLock)
    {
        // thao tác với người dùng
    }
}
// ... và ở chỗ khác lại ngược lại!
lock (userLock)
{
    lock (stockLock) { }
}

Ổn:

Thỏa thuận chung — luôn cố gắng khóa stockLock trước, rồi userLock trong mọi luồng.

6. Làm sao tránh Deadlock ở công ty và phỏng vấn

Trong dự án thực tế:

  • Khi thiết kế — vẽ sơ đồ luồng và lock (giấy/whiteboard đều ổn).
  • Chia sẻ quy ước thứ tự khóa với toàn team.
  • Dùng công cụ phân tích tĩnh (Rider/Visual Studio, ReSharper) — có thể tìm deadlock tiềm ẩn.
  • Đọc tài liệu trên Microsoft Docs về Deadlock.

Trong phỏng vấn:

  • Trình bày bạn hiểu vấn đề và các triệu chứng kinh điển.
  • Nhắc các cách bảo vệ: thứ tự cố định, timeout (TryEnter), giảm tối thiểu critical sections.
  • Cho ví dụ với TryEnter và giải thích vì sao quan trọng phải giải phóng tất cả resource trong finally.

7. Lỗi thường gặp và deadlock bất ngờ

Lỗi #1: lock không rõ ràng trong thư viện bên thứ ba.
Kịch bản kinh điển — logging trong critical section. Luồng kẹt, bạn còn không biết nguyên nhân vì là code ngoài.

Lỗi #2: khóa lại cùng một resource.
Đây là reentrant deadlock: luồng cố vào lại lock đã bị chiếm nhưng mechanism không hỗ trợ reentrancy.

Lỗi #3: dùng async trong critical section.
Đặc biệt nguy hiểm với await: luồng có thể “thả” control tại thời điểm không hợp lý và làm hệ thống kẹt.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Minh họa Deadlock
Minh họa Deadlock
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION