Hệ điều hành & Tiến trình/Deadlock — bốn điều kiện Coffman và cách phá vòng chờ
25/28
Bài 25 / 28~13 phútĐồng bộ & Phối hợpMiễn phí lượt xem

Deadlock — bốn điều kiện Coffman và cách phá vòng chờ

Hai luồng giữ khoá chờ nhau mãi mãi: 4 điều kiện Coffman để deadlock xảy ra, cách nhận diện qua thread dump, và phòng ngừa bằng lock ordering.

TL;DR: Deadlock là khi một nhóm luồng bị kẹt vĩnh viễn vì mỗi luồng đang giữ một khoá và chờ một khoá mà luồng khác trong nhóm đang giữ — vòng chờ khép kín, không ai nhả, không ai tiến. Nó chỉ xảy ra khi cả bốn điều kiện Coffman cùng đúng: mutual exclusion, hold-and-wait, no preemption, circular wait. Phá bất kỳ một điều kiện là đủ chống deadlock, và cách thực dụng nhất là phá circular wait bằng lock ordering — luôn lấy các khoá theo cùng một thứ tự toàn cục. Khi đã lỡ deadlock, jstack (thread dump) in thẳng "Found one Java-level deadlock" và chỉ ra vòng chờ. Khác race (kết quả sai), deadlock làm chương trình treo — thường dễ chẩn đoán hơn nhưng vẫn là sự cố nặng.

Ở bài trước, mutex trị được race. Nhưng khoá không miễn phí về rủi ro: dùng nhiều khoá không cẩn thận sinh ra một loại lỗi mới — chương trình không sai kết quả, nó đơn giản đứng im. Một service đang chạy bỗng ngừng xử lý request, CPU về 0%, không log, không exception. Đó thường là deadlock: hai luồng đang lịch sự chờ nhau, mãi mãi.

Bài này mổ đúng cơ chế đó: dựng một deadlock tối thiểu bằng hai khoá, chỉ ra bốn mảnh ghép phải cùng có để nó xảy ra, cách bắt nó tại hiện trường bằng thread dump, và cách thiết kế để nó không thể xảy ra. Học xong, bạn chẩn đoán được một service treo là deadlock qua thread dump, và thiết kế lock ordering để nó không thể xảy ra ngay từ đầu.

1. Analogy — hai người, một cây cầu một làn

Hai người đi xe máy vào một cây cầu hẹp một làn từ hai đầu, gặp nhau ở giữa. Muốn đi tiếp, mỗi người cần phần cầu người kia đang chiếm. Cả hai đều nghĩ: "Tôi tới trước, người kia lùi đi." Không ai lùi. Cả hai kẹt cứng — không phải vì hỏng xe, mà vì mỗi người giữ chỗ của mình và chờ chỗ của người kia.

Đây đúng là deadlock. Không ai làm gì sai theo luật riêng; hệ thống kẹt vì cấu trúc chờ đợi tạo thành vòng kín. Và cách phá cũng lộ ra từ analogy: nếu có luật "xe từ hướng Bắc luôn được ưu tiên" (một thứ tự toàn cục), sẽ không bao giờ có thế kẹt — người hướng Nam biết phải lùi.

Đời thường (cầu một làn)Chương trình đa luồng
Người đi xeLuồng (thread)
Nửa cầuMột khoá (lock/mutex)
Đang chiếm nửa cầuĐang giữ khoá
Chờ nửa cầu bên kiaBlock chờ khoá luồng khác giữ
Hai người chắn nhau vòng trònCircular wait
Luật ưu tiên một hướngLock ordering (thứ tự khoá toàn cục)
💡 Cách nhớ

Deadlock = giữ và chờ theo vòng tròn. A giữ khoá 1 chờ khoá 2; B giữ khoá 2 chờ khoá 1. Cắt vòng tròn ở bất kỳ đâu — đặc biệt bằng cách buộc mọi người lấy khoá theo cùng một thứ tự — là hết deadlock.

2. Dựng một deadlock tối thiểu

Cách chắc chắn nhất để tạo deadlock: hai luồng lấy hai khoá theo hai thứ tự ngược nhau. Ví dụ chuyển tiền giữa hai tài khoản, mỗi tài khoản có một khoá riêng:

public class DeadlockDemo {
    static final Object lockA = new Object();
    static final Object lockB = new Object();

    public static void main(String[] args) {
        // Luong 1: khoa A truoc, roi B
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                sleep(50);                 // tao khe thoi gian de chac chan ket
                synchronized (lockB) {     // cho B -- t2 dang giu
                    System.out.println("t1 got both");
                }
            }
        });
        // Luong 2: khoa B truoc, roi A  <-- THU TU NGUOC
        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                sleep(50);
                synchronized (lockA) {     // cho A -- t1 dang giu
                    System.out.println("t2 got both");
                }
            }
        });
        t1.start(); t2.start();
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) {}
    }
}
Thử đoán trước khi chạy

Hai luồng khoá chéo nhau: bạn nghĩ chương trình in ra gì — cả hai dòng "got both", một dòng, hay không dòng nào? Nó sẽ treo, crash, hay chạy xong bình thường? Và nếu treo, CPU lúc đó ở khoảng bao nhiêu phần trăm?

Chạy chương trình: nó in... không gì cả, rồi treo. t1 giữ lockA và chờ lockB; t2 giữ lockB và chờ lockA. Không ai nhả khoá đang giữ (vì còn kẹt trong synchronized), không ai lấy được khoá đang chờ. Chương trình đứng im vô hạn — CPU 0%, không crash.

sleep(50) chỉ để chắc chắn trúng deadlock mỗi lần cho mục đích minh hoạ. Trong thực tế, không cần sleep: chỉ cần scheduler xui xẻo cho t1 lấy lockAt2 lấy lockB trước khi luồng nào kịp lấy khoá thứ hai — và như race, timing đó thỉnh thoảng mới trúng, khiến deadlock production cũng rình rập bất chợt.

flowchart LR
  T1["Luong t1<br/>giu lockA"] -->|"cho lockB"| T2["Luong t2<br/>giu lockB"]
  T2 -->|"cho lockA"| T1

Đồ thị "ai chờ ai" có một vòng kín — đó chính là chữ ký hình học của deadlock.

3. Bốn điều kiện Coffman

Deadlock không xảy ra tuỳ tiện. Coffman, Elphick và Shoshani (1971) chứng minh nó cần đồng thời cả bốn điều kiện. Thiếu một, deadlock không thể hình thành:

  1. Mutual exclusion (loại trừ lẫn nhau): tài nguyên chỉ một luồng dùng tại một thời điểm. Khoá theo định nghĩa là vậy — nếu chia sẻ được thì đâu cần khoá.
  2. Hold-and-wait (giữ và chờ): một luồng đang giữ ít nhất một tài nguyên đang chờ xin thêm tài nguyên khác. Trong demo: t1 giữ lockA trong khi chờ lockB.
  3. No preemption (không thu hồi): không thể cưỡng đoạt tài nguyên từ luồng đang giữ; nó phải tự nguyện nhả. OS không thể "giật" khoá synchronized khỏi t1.
  4. Circular wait (chờ vòng tròn): tồn tại một chuỗi luồng T1 → T2 → ... → Tn → T1, trong đó mỗi luồng chờ tài nguyên do luồng kế trong chuỗi giữ. Demo: t1 → t2 → t1.
flowchart TB
  M["Mutual exclusion<br/>(khoa doc quyen)"] --> D(("DEADLOCK"))
  H["Hold-and-wait<br/>(giu + xin them)"] --> D
  N["No preemption<br/>(khong giat duoc)"] --> D
  C["Circular wait<br/>(cho vong tron)"] --> D

Điểm giải phóng nằm ở chữ cả bốn: vì deadlock cần tất cả bốn, ta chỉ cần phá một là chống được. Bốn điều kiện cho ta bốn hướng tấn công.

4. Phá điều kiện nào — và cách nào thực dụng nhất?

Xét từng điều kiện xem có phá được không:

  • Phá mutual exclusion? Gần như không — bản chất khoá là độc quyền. (Ngoại lệ: dùng cấu trúc lock-free/immutable để khỏi cần khoá — chính là "đừng chia sẻ" ở bài 02.)
  • Phá hold-and-wait? Yêu cầu luồng lấy tất cả khoá cần một lần ngay từ đầu (all-or-nothing); nếu không đủ thì buông hết, không giữ cái nào mà chờ. Khả thi nhưng khó khi không biết trước cần khoá gì.
  • Phá no-preemption? Cho phép "buông" khi chờ quá lâu: dùng tryLock(timeout) — nếu không lấy được khoá thứ hai trong hạn, nhả khoá đang giữ rồi thử lại từ đầu. Đây là cách ReentrantLock hỗ trợ trực tiếp.
  • Phá circular wait? Áp một thứ tự toàn cục lên mọi khoá và buộc mọi luồng lấy khoá theo đúng thứ tự tăng dần đó. Không thể có vòng kín nếu mọi người đi cùng một chiều. Đây là cách phổ biến và rẻ nhất.

Lock ordering (phá circular wait). Gán cho mỗi khoá một thứ hạng (ví dụ theo id tài khoản) và luôn khoá cái hạng nhỏ trước. (Khi không có id tự nhiên, có thể dùng System.identityHashCode làm thứ hạng, nhưng nó không duy nhất — hai object có thể trùng hash, khiến thứ tự không toàn phần; đúng lúc trùng, JCiP §10.1.2 phải thêm một "tie-breaker lock" thứ ba để phá thế hoà.) Trước khi xem lời giải, thử tự điền hai chỗ trống dưới đây:

static void transfer(Account from, Account to, long amount) {
    // Muc tieu: luon khoa tai khoan id NHO hon truoc, bat ke ai la nguon/dich
    Account first  = /* TODO: chon tai khoan nao khoa truoc? */;
    Account second = /* TODO: chon tai khoan nao khoa sau? */;
    synchronized (first.lock()) {
        synchronized (second.lock()) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}
Tự điền trước khi xem đáp án

Bạn cần một thứ tự toàn cục mà cả transfer(A, B) lẫn transfer(B, A) đều tuân theo. Manh mối: mỗi Accountid(). Hai dòng first/second phải chọn thế nào để bất kể ai là nguồn/đích, khoá lấy trước luôn rơi vào cùng một tài khoản?

Đáp án — sắp hai tài khoản theo id tăng dần, luôn khoá cái nhỏ trước:

static void transfer(Account from, Account to, long amount) {
    // Luon khoa theo thu tu id tang dan -> khong bao gio tao vong kin
    Account first  = from.id() < to.id() ? from : to;
    Account second = from.id() < to.id() ? to : from;
    synchronized (first.lock()) {
        synchronized (second.lock()) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}

Giờ dù transfer(A, B)transfer(B, A) chạy song song, cả hai đều khoá tài khoản id nhỏ trước — không còn hai thứ tự ngược nhau, không còn vòng kín. Deadlock bị loại về mặt cấu trúc.

tryLock (phá no-preemption). Khi không áp được thứ tự toàn cục (khoá đến từ nhiều thư viện, không so sánh được), dùng chờ-có-hạn rồi rút lui:

// tryLock(timeout) va Thread.sleep deu nem InterruptedException -> khai bao throws
void transfer() throws InterruptedException {
    while (true) {
        if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
                    try { /* critical section */ return; }
                    finally { lockB.unlock(); }
                }
            } finally { lockA.unlock(); }   // lay B that bai -> nha A, thu lai
        }
        // backoff ngau nhien de tranh hai luong cu retry dong bo
        Thread.sleep(ThreadLocalRandom.current().nextInt(50));
    }
}

Cách này không kẹt vĩnh viễn (luôn có đường buông ra), nhưng phức tạp hơn và có thể livelock nếu các luồng cứ retry đồng bộ — nên thêm backoff ngẫu nhiên. Ưu tiên lock ordering; dùng tryLock khi không còn cách.

Livelock — anh em của deadlock

Phá deadlock ẩu có thể sinh livelock: các luồng không kẹt cứng mà cứ nhường nhau qua lại mãi (như hai người trong hành lang cùng bước sang một bên, rồi cùng bước lại) — chúng vẫn chạy (CPU không về 0) nhưng không ai tiến. Backoff ngẫu nhiên trong tryLock là để phá tính đồng bộ này. Deadlock treo, livelock quay cuồng — cả hai đều là "không tiến được".

5. Chẩn đoán — đọc thread dump

Khi một service Java treo, công cụ đầu tiên là thread dump — ảnh chụp mọi luồng và khoá chúng đang giữ/chờ. Lấy bằng jstack <pid> (hoặc gửi tín hiệu SIGQUIT/Ctrl-\). JVM tự phát hiện deadlock chu trình khoá synchronized/ReentrantLock và in thẳng ra đầu dump:

Found one Java-level deadlock:
=============================
"Thread-0":
  waiting to lock monitor 0x00007f... (object 0x000000..., a java.lang.Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x00007f... (object 0x000000..., a java.lang.Object),
  which is held by "Thread-0"

Java stack information for the threads listed above:
===================================================
"Thread-0":
        at DeadlockDemo.lambda$main$0(DeadlockDemo.java:11)
        - waiting to lock <0x...> (a java.lang.Object)
        - locked <0x...> (a java.lang.Object)
"Thread-1":
        at DeadlockDemo.lambda$main$1(DeadlockDemo.java:19)
        - waiting to lock <0x...> (a java.lang.Object)
        - locked <0x...> (a java.lang.Object)

Đọc dump: dòng Found one Java-level deadlock là kết luận sẵn của JVM. Với mỗi luồng, locked <...> là khoá nó đang giữ, waiting to lock <...> là khoá nó đang chờ. Ghép lại thấy Thread-0 chờ khoá Thread-1 giữ và ngược lại — đúng vòng kín. Số dòng (DeadlockDemo.java:11) chỉ thẳng chỗ code kẹt để bạn sửa.

Vì sao deadlock 'dễ' hơn race một chút

Khác race (kết quả sai âm thầm, Heisenbug — bài 01), deadlock khiến chương trình treo có thể quan sát: CPU về 0, luồng ở trạng thái BLOCKED, và JVM tự chỉ ra vòng khoá trong thread dump. Bạn không phải đoán interleaving — bằng chứng nằm sẵn trong dump. Nhược điểm: nó chỉ phát hiện được deadlock qua khoá JVM biết; deadlock qua tài nguyên ngoài (kết nối DB, semaphore hệ điều hành) thì JVM không thấy, phải tự lần.

6. Áp dụng vào code của bạn

  • Đếm số khoá một luồng giữ đồng thời. Deadlock cần giữ ≥2 khoá cùng lúc (hold-and-wait). Nếu mỗi luồng chỉ bao giờ giữ một khoá tại một thời điểm, deadlock không thể xảy ra. Cách rẻ nhất chống deadlock là không lồng khoá.
  • Khi buộc lồng khoá, áp lock ordering. Định một thứ tự toàn cục (theo id, tên, hay hằng số) và luôn lấy theo thứ tự đó ở mọi nơi trong codebase. Chỉ một chỗ lấy ngược là đủ mở lại cửa deadlock.
  • Đừng gọi code lạ trong khi giữ khoá. Gọi callback, listener, hay method của lớp con khi đang giữ khoá là nguy hiểm: code đó có thể lấy khoá khác và tạo vòng bạn không lường trước. Nhả khoá trước khi gọi ra ngoài.
  • Ưu tiên không chia sẻ (bài 02). Message passing và immutable không cần khoá → không có deadlock. Deadlock là cái giá của việc chia sẻ dữ liệu bằng khoá; tránh chia sẻ thì tránh luôn cái giá đó.

7. Pitfall tổng hợp

Nhầm 1 — hai method khoá cùng hai object theo thứ tự khác nhau:

void moveLeftToRight() { synchronized (a) { synchronized (b) { ... } } }
void moveRightToLeft() { synchronized (b) { synchronized (a) { ... } } }  // NGUOC

✅ Hai method này chạy song song là deadlock chờ sẵn. Sửa: cả hai lấy khoá theo cùng thứ tự toàn cục (luôn a trước b, bất kể hướng di chuyển). Thứ tự khoá phải nhất quán toàn codebase, không theo ngữ nghĩa lời gọi.

Nhầm 2 — "thêm sleep/retry cho đỡ đụng" mà không phá điều kiện nào:

✅ Chèn sleep chỉ đổi timing, làm deadlock hiếm hơn chứ không hết — y như thêm log không sửa được race. Deadlock chỉ biến mất khi bạn phá một trong bốn điều kiện Coffman một cách có chủ đích (thường là lock ordering). Đừng nhầm "hiếm hơn" với "đã sửa".

Nhầm 3 — giữ khoá rồi gọi ra ngoài (callback/lock lồng ẩn):

synchronized (lock) {
    listener.onChange(this);   // listener co the lay khoa khac -> vong an
}

✅ Bạn không kiểm soát listener làm gì; nó có thể lấy một khoá tạo vòng chờ với lock. Thu thập dữ liệu cần trong khoá, nhả khoá, rồi mới gọi callback ngoài.

8. 📚 Deep Dive — deadlock trong lý thuyết OS

📚 Deep Dive (tuỳ chọn)

Nguồn chính:

  • OSTEP — Chapter 32: Concurrency Bugs (Arpaci-Dusseau) — mục 32.3 "Deadlock Bugs" trình bày đúng bốn điều kiện và các chiến lược phá (prevention qua total ordering, avoidance qua Banker's algorithm, detect-and-recover). Đọc để thấy lock ordering là một trong nhiều lựa chọn và vì sao nó thắng về tính thực dụng.
  • Coffman, Elphick, Shoshani, "System Deadlocks", ACM Computing Surveys 3(2), 1971 — bài báo gốc định nghĩa bốn điều kiện mang tên Coffman.

Ghi chú: Ba hướng xử lý deadlock trong lý thuyết OS — prevention (thiết kế để một điều kiện không bao giờ đúng, như lock ordering), avoidance (từ chối cấp tài nguyên nếu dẫn tới trạng thái nguy hiểm, như Banker's algorithm), và detection + recovery (để nó xảy ra rồi phát hiện và phá, như JVM in thread dump + bạn kill/restart). Ứng dụng thực tế gần như luôn dùng prevention (lock ordering) vì avoidance đòi biết trước nhu cầu tài nguyên, hiếm khi khả thi.

9. Liên hệ các bài khác

  • Bài 02 — Mutex & atomic: deadlock là mặt trái của mutex; tryLock (giới thiệu ở bài 02) chính là công cụ phá no-preemption ở đây.
  • Bài 01 — Race condition: cặp song sinh — race là kết quả sai, deadlock là chương trình treo; cả hai đều timing-dependent và đến từ chia sẻ dữ liệu.
  • Bài 04 — IPC: message passing tránh chia sẻ khoá nên tránh được deadlock do tranh khoá — dù hai bên vẫn có thể kẹt nếu cùng block chờ nhận của nhau (communication deadlock); một lý do nữa để cân nhắc kiến trúc không-chia-sẻ.
  • Bài 05 — Mini-challenge: bạn sẽ chẩn đoán và phá một deadlock thật bằng lock ordering.

10. Tóm tắt

  • Deadlock: mỗi luồng giữ một khoá và chờ khoá luồng khác giữ, tạo vòng chờ khép kín → treo vĩnh viễn (CPU 0%, không crash).
  • Cần cả bốn điều kiện Coffman cùng đúng: mutual exclusion, hold-and-wait, no preemption, circular wait.
  • Vì cần cả bốn, phá một là đủ. Thực dụng nhất: phá circular wait bằng lock ordering — luôn lấy khoá theo cùng thứ tự toàn cục.
  • tryLock(timeout) phá no-preemption: chờ có hạn rồi nhả hết và thử lại (kèm backoff ngẫu nhiên để tránh livelock).
  • jstack in "Found one Java-level deadlock" và chỉ ra locked/waiting to lock của từng luồng — bằng chứng vòng chờ nằm sẵn trong thread dump.
  • Deadlock dễ chẩn đoán hơn race (treo quan sát được, JVM tự chỉ) nhưng chỉ thấy được deadlock qua khoá JVM biết.
  • Chống rẻ nhất: không lồng khoá (mỗi luồng giữ tối đa một khoá); tốt hơn nữa: không chia sẻ (message passing, immutable) → không cần khoá → không deadlock.

11. Tự kiểm tra

Tự kiểm tra
Q1
Deadlock cần cả bốn điều kiện Coffman. Vì sao điều đó lại là tin tốt cho người muốn chống deadlock?

Vì deadlock chỉ hình thành khi tất cả bốn điều kiện (mutual exclusion, hold-and-wait, no preemption, circular wait) cùng đúng, ta không phải triệt tiêu cả bốn — chỉ cần làm cho một điều kiện không bao giờ đúng là deadlock không thể xảy ra.

Điều này cho bốn hướng tấn công. Thực dụng nhất là phá circular wait bằng lock ordering (luôn lấy khoá theo cùng thứ tự toàn cục) vì nó rẻ và cục bộ. Phá hold-and-wait (lấy tất cả khoá một lần) hay no-preemption (tryLock rồi buông) cũng được nhưng phức tạp hơn. Phá mutual exclusion gần như bất khả trừ khi bỏ khoá hẳn (dùng immutable/lock-free).

Q2
Cho transfer(A, B)transfer(B, A) chạy song song, mỗi lệnh khoá tài khoản nguồn trước rồi đích. Deadlock xảy ra thế nào, và lock ordering sửa ra sao?

transfer(A, B) khoá A rồi chờ B; transfer(B, A) khoá B rồi chờ A. Nếu cả hai kịp lấy khoá đầu trước khi luồng kia lấy khoá thứ hai, ta có vòng: luồng 1 giữ A chờ B, luồng 2 giữ B chờ A — circular wait, treo. Đây là hai thứ tự khoá ngược nhau do thứ tự phụ thuộc tham số lời gọi.

Lock ordering phá vòng: gán mỗi tài khoản một thứ hạng (ví dụ theo id) và luôn khoá cái id nhỏ hơn trước, bất kể ai là nguồn/đích. Khi đó cả hai lời gọi đều khoá cùng một tài khoản trước — không còn hai thứ tự ngược, không thể có vòng kín. Deadlock bị loại về mặt cấu trúc, không phải giảm xác suất.

Q3
Một service Java "treo": không xử lý request, CPU về 0%, không exception. Bạn làm gì để xác nhận deadlock và tìm chỗ kẹt?

Lấy thread dump: jstack <pid> (hoặc gửi SIGQUIT). JVM tự phát hiện chu trình khoá và in Found one Java-level deadlock ngay đầu dump — đó là xác nhận sẵn, không phải đoán.

Đọc phần liệt kê: mỗi luồng có dòng locked <...> (khoá đang giữ) và waiting to lock <...> (khoá đang chờ). Ghép lại thấy vòng chờ (luồng 0 chờ khoá luồng 1 giữ và ngược lại). Số dòng stack (như File.java:11) chỉ thẳng chỗ code kẹt để sửa. Triệu chứng CPU 0% + BLOCKED phân biệt deadlock với livelock (CPU cao, quay cuồng) và với treo do I/O.

Q4
tryLock(timeout) phá điều kiện Coffman nào, và vì sao nó có thể sinh livelock nếu không cẩn thận?

Nó phá no-preemption: thay vì chờ khoá thứ hai vô hạn (giữ khư khư khoá đầu), luồng chờ có hạn; nếu quá hạn thì tự nhả khoá đang giữ và thử lại từ đầu. Việc "tự buông" mô phỏng preemption tự nguyện, nên không còn giữ-và-chờ vĩnh viễn.

Rủi ro livelock: nếu hai luồng cùng thất bại, cùng nhả, rồi cùng retry đồng bộ, chúng có thể lặp lại đúng thế đụng độ mãi — vẫn chạy (CPU cao) nhưng không ai tiến. Cách phá: thêm backoff ngẫu nhiên trước khi retry để lệch nhịp hai luồng. Vì sự phức tạp này, lock ordering (phá circular wait) thường được ưu tiên hơn tryLock.

Q5
Vì sao "mỗi luồng chỉ giữ tối đa một khoá tại một thời điểm" là cách chống deadlock rẻ nhất?

Deadlock cần điều kiện hold-and-wait: một luồng phải đang giữ một khoá và chờ khoá khác. Nếu mỗi luồng không bao giờ giữ quá một khoá — lấy một khoá, làm việc, nhả, rồi mới lấy khoá khác — thì không có luồng nào "giữ và chờ", nên không thể tạo vòng chờ. Điều kiện hold-and-wait bị triệt tiêu.

Rẻ vì nó chỉ đòi một kỷ luật thiết kế đơn giản (không lồng khoá), không cần thứ tự toàn cục hay logic retry. Khi buộc phải giữ nhiều khoá thì mới cần tới lock ordering. Rẻ hơn nữa là không chia sẻ dữ liệu (immutable, message passing) để khỏi cần khoá nào.

Q6
Vì sao gọi một callback/listener trong khi đang giữ khoá là nguy hiểm cho deadlock?

Khi giữ khoá L và gọi ra code bạn không kiểm soát (listener, callback, method override của lớp con), code đó có thể tự lấy một khoá M khác. Nếu ở đâu đó trong hệ thống có luồng giữ M rồi chờ L, bạn vừa tạo một vòng chờ ẩn mà không hề thấy trong code của mình — thứ tự khoá bị "code lạ" phá vỡ.

Vì bạn không biết callback sẽ lấy khoá gì, không thể đảm bảo lock ordering. Cách an toàn: thu thập mọi dữ liệu cần trong khoá, nhả khoá, rồi mới gọi callback ra ngoài. Nguyên tắc: đừng bao giờ giữ khoá khi gọi ra ranh giới bạn không kiểm soát.

Bài tiếp theo: IPC — pipe, shared memory, socket

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên