CHƯƠNG 07 · CONCURRENCY · ~100 phút

Deadlock
"Tê liệt vĩnh viễn"

Một trong những bug khó tìm nhất. Học chương này, bạn nhận diện được dấu hiệu deadlock, biết 4 điều kiện Coffman, và biết cách phòng tránh từ đầu (lock ordering, timeout) thay vì để xảy ra.

7.1 Deadlock là gì?

Deadlock = tình huống 2+ process/thread đợi nhau vĩnh viễn — không ai chạy được nữa. Hệ thống "đứng hình" ở phần đó.

Ví dụ kinh điển

// Thread 1
pthread_mutex_lock(&A);   // chiếm A
sleep(1);
pthread_mutex_lock(&B);   // đợi B (đang bị Thread 2 giữ)

// Thread 2
pthread_mutex_lock(&B);   // chiếm B
sleep(1);
pthread_mutex_lock(&A);   // đợi A (đang bị Thread 1 giữ)

// → Cả 2 thread đợi nhau mãi mãi. DEADLOCK.
Thread 1 Thread 2 ──────── ──────── holds A ──── waits ────▶ holds B ◀──── waits ──── Vòng tròn chờ đợi → deadlock

7.2 4 điều kiện Coffman — phải có ĐỦ 4 mới deadlock

Edward Coffman (1971) chứng minh deadlock xảy ra khi và chỉ khi đủ 4 điều kiện:

  1. Mutual Exclusion (loại trừ lẫn nhau): Tài nguyên chỉ cấp được cho 1 process tại 1 thời điểm (vd mutex, file lock).
  2. Hold & Wait: Process đang giữ ít nhất 1 resource và đang chờ resource khác (đang bị process khác giữ).
  3. No Preemption: Resource không thể bị "cướp" — chỉ process đang giữ mới giải phóng được.
  4. Circular Wait: Có vòng tròn chờ đợi: P1 → P2 → ... → Pn → P1.
📐 Quan trọng

Phá 1 trong 4 điều kiện = không có deadlock. Đây là cơ sở của các kỹ thuật Prevention (xem 7.4).

7.3 Resource Allocation Graph (RAG)

Cách trực quan để phát hiện deadlock: vẽ graph với:

  • Node P = process; R = resource
  • Cạnh P → R: process P đang đợi resource R
  • Cạnh R → P: resource R đang được P giữ

Có cycle trong graph = có khả năng deadlock (single instance) hoặc deadlock chắc chắn (1 instance / resource).

Deadlock state: ┌──── holds ────┐ P1 ─┤ │ └────waits────▶ R2 ◀── holds ──┐ │ P2 ┌────waits────▶ R1 ◀── holds ───┤ P2 ─┤ └──────────────┘ Có cycle P1 → R2 → P2 → R1 → P1 → DEADLOCK

7.4 Prevention — phá 1 trong 4 điều kiện

Phá Mutual Exclusion

Không thể cho mutex/lock — vì bản chất chúng là exclusive. Có thể với một số resource (vd file đọc-only).

Phá Hold & Wait

Bắt process xin tất cả resource cùng lúc hoặc không xin gì.

// Thay vì
pthread_mutex_lock(&A);
pthread_mutex_lock(&B);

// Dùng try_lock_all hoặc xin từ đầu
if (try_lock_both(&A, &B)) {
    // ok, thành công
} else {
    // không có gì, thử lại sau
}

Nhược: utilization thấp (process giữ resource lâu mà không dùng), có thể starve.

Phá No Preemption

Cho phép "cướp" resource từ process. Khó với mutex/file. Khả thi với CPU, memory page.

Phá Circular Wait — phổ biến nhất, hiệu quả nhất

Áp đặt thứ tự tổng trên các resource. Process phải lock theo thứ tự tăng dần.

// Tất cả thread phải lock A trước B (theo thứ tự id)
// Thread 1
lock(A); lock(B); ... unlock(B); unlock(A);
// Thread 2 (đáng lẽ lock B rồi A → fix lại):
lock(A); lock(B); ... unlock(B); unlock(A);

// Đảm bảo: không có circular dependency vì mọi thread cùng order

Đây là Lock Ordering — kỹ thuật prevention phổ biến nhất trong production.

7.5 Avoidance — Banker's Algorithm

Trước khi cấp resource cho process, kiểm tra xem việc cấp có dẫn đến state có thể deadlock không. Nếu có nguy cơ → từ chối cấp ngay cả khi resource đang free.

Banker's Algorithm (Dijkstra)

Mỗi process khai báo trước nhu cầu tối đa mỗi loại resource. OS theo dõi:

  • Allocation[i]: process i đang giữ bao nhiêu
  • Max[i]: tối đa cần
  • Need[i] = Max[i] - Allocation[i]: còn cần
  • Available: resource còn dư

Khi có request, OS chạy Safety Check: thử cấp giả + xem có sequence hoàn thành mọi process được không. Nếu có → "safe state" → cấp. Nếu không → từ chối.

Trong thực tế ít dùng vì đòi hỏi biết trước max need (khó), và overhead chạy safety check mỗi lần. Mostly academic.

7.6 Detection & Recovery

Cho phép deadlock xảy ra, định kỳ chạy thuật toán phát hiện (vd RAG cycle), recovery khi phát hiện.

Detection

  • Build RAG, tìm cycle (DFS) → có cycle = (có thể) deadlock
  • Linux: lockdep tool trong kernel detect lock ordering bug
  • Database: thread monitor đếm thời gian wait, detect cycle qua lock graph

Recovery

  1. Process termination: kill 1 process trong cycle
  2. Resource preemption: cướp resource từ 1 process, rollback nó

Database (MySQL/PostgreSQL): phát hiện deadlock → chọn 1 transaction "victim" (thường ưu tiên kill transaction trẻ hơn, ít cost rollback hơn) → rollback nó. App nhận error, retry.

7.7 Lock Ordering — best practice trong production code

Đây là kỹ thuật thực tế nhất để phòng deadlock. Quy tắc:

💡 Quy tắc vàng

Tất cả thread phải lock các resource theo cùng 1 thứ tự (thường là theo địa chỉ memory hoặc ID).

// Sai: thứ tự lock khác nhau
void transfer(Account *from, Account *to, int amount) {
    lock(&from->mutex);
    lock(&to->mutex);
    // ...
}
// transfer(A, B) lock A→B
// transfer(B, A) lock B→A
// → 2 thread chạy song song → DEADLOCK

// Đúng: lock theo address
void transfer(Account *from, Account *to, int amount) {
    Account *first  = (from < to) ? from : to;
    Account *second = (from < to) ? to : from;
    lock(&first->mutex);
    lock(&second->mutex);
    // ...
    unlock(&second->mutex);
    unlock(&first->mutex);
}

Timeout-based Detection

// pthread_mutex_timedlock — fail nếu không lock được trong T giây
struct timespec ts = {.tv_sec = 5};
if (pthread_mutex_timedlock(&mutex, &ts) != 0) {
    // có thể đang deadlock — log + retry với strategy khác
    handle_potential_deadlock();
}

Many DB drivers, distributed systems dùng timeout làm "phao cứu sinh".

7.8 Deadlock vs Livelock vs Starvation

Deadlock

Process bị block vĩnh viễn, đợi nhau. CPU usage = 0% ở thread đó.

Vd: 2 thread đợi mutex của nhau.

Livelock

Process đang chạy nhưng không tiến triển. CPU usage cao mà không xong việc.

Vd: 2 người tránh nhau ở hành lang — cứ né cùng hướng. 2 thread cùng "lùi" rồi "tiến" liên tục.

Starvation

Process bị treo không vô hạn nhưng rất lâu, do priority thấp / không công bằng.

Vd: pure priority scheduling, low-priority process bị bỏ rơi.

Sự khác biệt: deadlock = không ai chạy; livelock = tất cả chạy nhưng vô ích; starvation = một số chạy, một số bị bỏ rơi.

7.9 Database Deadlock

DB như MySQL/PostgreSQL có lock cấp row/table. Hai transaction có thể deadlock:

-- Tx1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- (chiếm row 1)
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- (đợi row 2)

-- Tx2 (chạy đồng thời)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
-- (chiếm row 2)
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
-- (đợi row 1) → DEADLOCK

DB tự xử lý ra sao?

  • DB engine định kỳ check deadlock (mỗi vài giây hoặc on-demand)
  • Build wait-for graph, detect cycle
  • Chọn "victim" transaction, rollback. App nhận error: "Deadlock detected"
  • App nên retry transaction sau vài ms
// Pattern retry trong code
async function transferWithRetry(from, to, amount, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await db.transaction(async (tx) => {
        await tx.query('UPDATE accounts ... WHERE id = ?', [from]);
        await tx.query('UPDATE accounts ... WHERE id = ?', [to]);
      });
    } catch (e) {
      if (e.code === 'ER_LOCK_DEADLOCK' && i < retries - 1) {
        await new Promise(r => setTimeout(r, 50 * (i + 1)));
        continue;
      }
      throw e;
    }
  }
}

Phòng tránh DB deadlock

  • Always lock rows in same order (vd ORDER BY id)
  • Giữ transaction ngắn — ít chiếm lock lâu
  • Dùng SELECT FOR UPDATE sớm để chiếm lock đầu
  • Tránh transaction phức tạp với nhiều bảng

Bài tập

Bài 1 — Tạo deadlock

Viết C/Java program với 2 thread + 2 mutex. Cố tình tạo deadlock. Quan sát: cả 2 thread đứng hình, CPU 0%.

Bài 2 — Fix bằng lock ordering

Sửa bài 1 bằng cách lock theo address mutex tăng dần. Verify không deadlock.

Bài 3 — Phá điều kiện Coffman

4 điều kiện Coffman. Cho 1 ví dụ thực tế phá mỗi điều kiện trong production code.

Bài 4 — DB deadlock với MySQL/PostgreSQL

Mở 2 mysql session. Bắt đầu transaction trong cả 2. Tạo deadlock bằng cách UPDATE chéo. Quan sát error.

Bài 5 — Livelock vs Deadlock

Code 2 thread gây livelock (không deadlock). Vd: 2 người tránh nhau (cùng "step left").

Bài 6 — Banker's algorithm

Cho ma trận Allocation, Max, Available. Viết code kiểm tra "safe state" theo Banker's.

🧪 Quiz cuối chương

Câu 1. 4 điều kiện Coffman gồm những gì?

  • Mutex, Semaphore, Monitor, Spinlock
  • Critical Section, Race Condition, Lock, Unlock
  • Mutual Exclusion, Hold & Wait, No Preemption, Circular Wait
  • Process, Thread, Resource, Allocation

Đáp án: ME, Hold&Wait, No Preemption, Circular Wait. Phải đủ 4 mới deadlock; phá 1 cái = an toàn.

Câu 2. Kỹ thuật prevention phổ biến nhất trong production?

  • Banker's Algorithm
  • Lock Ordering — phá Circular Wait
  • Tăng số resource
  • Giảm số process

Đáp án: Lock ordering. Đơn giản, hiệu quả, không cần biết trước max need như Banker's.

Câu 3. Deadlock vs Livelock khác nhau?

  • Deadlock: thread block, CPU 0%; Livelock: thread chạy nhưng không tiến triển, CPU cao
  • Deadlock chỉ trên Linux
  • Livelock không có thật
  • Hai cái giống nhau

Đáp án: deadlock = stuck, livelock = chạy vô ích. Cả 2 đều không tiến, nhưng cách khác nhau.

Câu 4. Database (MySQL/PostgreSQL) xử lý deadlock thế nào?

  • Restart cả server
  • Đợi infinite
  • Detect cycle, kill 1 transaction (rollback), trả error cho app — app retry
  • Không thể xảy ra trong DB

Đáp án: detect + kill victim + retry. App phải handle deadlock error và retry.

Câu 5. Banker's Algorithm nhược điểm gì khiến ít dùng thực tế?

  • Quá phức tạp
  • Chỉ chạy trên Linux
  • Phải biết trước max need của mọi process — khó trong app thực
  • Tốn 100% CPU

Đáp án: phải biết trước max need. App thực tế khó khai báo "tôi cần tối đa X mutex". Mostly academic.

Câu 6. Trong Resource Allocation Graph, cycle nghĩa là?

  • Mọi thứ ổn
  • Có khả năng deadlock (chắc chắn deadlock nếu mỗi resource chỉ 1 instance)
  • Process bị treo do priority
  • Resource leak

Đáp án: deadlock. Cycle = vòng tròn chờ đợi. Multiple instances per resource → cần check thêm.

Câu 7. Pattern code transfer(A, B) để tránh deadlock?

  • Lock A trước, sau đó B
  • Lock B trước, sau đó A
  • Lock theo địa chỉ tăng dần — đảm bảo mọi caller cùng order
  • Không cần lock

Đáp án: order theo address. Nếu transfer(A,B) lock A→B, transfer(B,A) cũng lock A→B (cùng order) → no circular wait.

Câu 8. Khi gặp DB deadlock error, app nên?

  • Retry transaction sau delay nhỏ (vài ms ~ vài trăm ms)
  • Crash app
  • Restart database
  • Ignore error

Đáp án: retry với backoff. Deadlock thường tạm thời — retry thường thành công. Có thể exponential backoff nếu retry lần đầu fail.

Tổng kết chương 7

  • ✅ Deadlock = 2+ process đợi nhau vĩnh viễn, không ai tiến triển
  • 4 điều kiện Coffman: Mutual Exclusion + Hold&Wait + No Preemption + Circular Wait. Phá 1 = an toàn.
  • Resource Allocation Graph (RAG): cycle → deadlock
  • ✅ Phòng tránh: Prevention (phá điều kiện), Avoidance (Banker's), Detection + Recovery
  • Lock Ordering là kỹ thuật phổ biến nhất trong production
  • ✅ Deadlock (stuck) ≠ Livelock (chạy vô ích) ≠ Starvation (bỏ rơi)
  • ✅ DB deadlock: tự detect + kill victim transaction → app retry
  • ✅ Best practice code: lock theo address, dùng try_lock với timeout
← Chương trước Chương 06: Synchronization Chương kế tiếp Chương 08: Memory Management →