OS · บทที่ 5 Synchronization

หัวข้อ 14 · 18 นาที

Deadlock

นึกภาพก่อน

Dining Lawyers: ทนายนั่งรอบโต๊ะกลม มีตะเกียบวางคั่นระหว่างคนละหนึ่งข้าง แต่ละคนต้องใช้ตะเกียบ สองข้าง จึงจะกินได้ และทุกคน หยิบข้างขวาก่อน

ถ้าทุกคนหยิบข้างขวาพร้อมกัน ทุกคนถือหนึ่งข้างและรอข้างซ้าย ซึ่งอยู่ในมือของคนข้าง ๆ ที่ก็กำลังรอเหมือนกัน ไม่มีใครวาง ไม่มีใครได้กิน ตลอดกาล

นิยาม

Resource คือสิ่ง (ที่เป็นฝ่ายถูกใช้) ที่ thread ต้องการเพื่อทำงาน เช่น CPU, disk space, memory, lock

ชนิดความหมายตัวอย่าง
PreemptableOS ยึดคืนได้CPU
Non-preemptableต้องอยู่กับ thread จนกว่ามันจะคืนเองlock
  • Starvation: thread รอ ไม่มีกำหนด
  • Deadlock: การรอ resource เป็นวง (circular waiting)

Deadlock ⇒ starvation แต่กลับกันไม่จริง

thread ที่ติด deadlock รอไม่มีกำหนดแน่นอน จึงเป็น starvation ด้วย แต่ starvation เกิดโดยไม่มีวงได้ เช่น thread priority ต่ำที่ไม่เคยได้ CPU เพราะมีงาน priority สูงเข้ามาเรื่อย ๆ — ระบบยังเดินอยู่ และ thread นั้น อาจ ได้ทำงานถ้างานอื่นหมด ส่วน deadlock ไม่มีทางหลุดเองเลย

ตัวอย่าง: สอง lock

c
// Thread A                 // Thread B
lock1.acquire();            lock2.acquire();
lock2.acquire();            lock1.acquire();
lock2.release();            lock1.release();
lock1.release();            lock2.release();

ถ้า A ได้ lock1 แล้ว B ได้ lock2 ก่อนที่ A จะขอ lock2: A รอ lock2 (B ถือ) และ B รอ lock1 (A ถือ) → deadlock

ถ้า A ทำสองบรรทัดแรกเสร็จก่อนที่ B จะเริ่ม ก็ไม่เกิด deadlock เกิดหรือไม่ขึ้นกับ schedule จึงทดสอบแล้วอาจไม่เจอ

ขั้นที่ 1 / 6เริ่มต้น

+n = acquire lock n, -n = release lock n เช่น A: +1 +2 -2 -1

ชื่อ thread แต่ละตัว = ให้ทำ 1 คำสั่ง เมื่อลำดับหมด thread ที่เหลือทำต่อจนจบหรือติด

ลำดับการ acquire
  1. A
  2. B
  3. A
  4. B
ABL1freeL2free
  • A+L1+L2−L2−L1
  • B+L2+L1−L1−L2
━▶ lock → thread: ถืออยู่┅▶ thread → lock: รออยู่
1.0×

คำอธิบายทีละขั้น

ยังไม่มีใครถือ lock

วงกลมคือ thread สี่เหลี่ยมคือ lock (resource) เส้นทึบจาก lock ไป thread = thread ถือ lock นั้นอยู่ เส้นประจาก thread ไป lock = thread กำลังขอและรอ lock นั้น ถ้าเส้นต่อกันเป็นวง แปลว่าเกิด deadlock

ลองใน simulation:

  • "สไลด์ p.16: สอง lock" สลับ A B A B → เกิดวง
  • "สอง lock: โชคดี" โปรแกรมเดิม แต่ A ทำเสร็จก่อน → ไม่เกิด
  • "Dining Lawyers" ทุกคนหยิบข้างขวาก่อน → วงสามคน
  • "lock ordering" โปรแกรมเดิม ลำดับการสลับเดิม แต่ทุก thread acquire เรียงตามหมายเลข → ไม่เกิด

ตัวอย่าง: สอง lock กับ condition variable

c
// Thread A                          // Thread B
lock1.acquire();                     lock1.acquire();
…                                    …
lock2.acquire();                     lock2.acquire();
while (need to wait) {               …
    condition.wait(lock2);           condition.signal(lock2);
}                                    …
lock2.release();                     lock2.release();
…                                    …
lock1.release();                     lock1.release();

ทั้งสอง thread acquire ในลำดับ เดียวกัน (lock1 แล้ว lock2) แต่ยัง deadlock ได้:

  1. A ถือ lock1 และ lock2 แล้วเรียก condition.wait(lock2)
  2. wait ปล่อย เฉพาะ lock2 — A ยังถือ lock1 ระหว่างหลับ
  3. B ต้อง acquire lock1 ก่อนจึงจะไปถึงบรรทัด signal ได้ แต่ lock1 อยู่กับ A
  4. A รอ signal จาก B ส่วน B รอ lock1 จาก A → deadlock

บทเรียน: การรอ condition variable ขณะถือ lock อื่นอยู่ก็เป็น "wait while holding" แบบหนึ่ง

เงื่อนไขจำเป็นสี่ข้อ

deadlock เกิดได้ก็ต่อเมื่อมี ครบทั้งสี่ข้อ:

#เงื่อนไขความหมายถ้าไม่มีข้อนี้
1Limited access to resourcesresource มีจำกัดถ้า resource ไม่จำกัด ไม่มี deadlock!
2No preemptionยึด resource คืนจากผู้ถือไม่ได้ถ้า resource เป็น virtual ก็ทำลาย deadlock ได้
3Multiple independent requests"wait while holding" — ถือของอยู่แล้วรอขอเพิ่มถ้าขอทีเดียวทั้งหมด หรือปล่อยก่อนขอ ก็ไม่ค้าง
4Circular chain of requestsการรอต่อกันเป็นวงถ้าไม่มีวง จะมีอย่างน้อยหนึ่ง thread ที่ทำต่อได้

เป็นเงื่อนไข จำเป็น ไม่ใช่เงื่อนไขเพียงพอสำหรับสามข้อแรก: มีสามข้อแรกครบก็ยังไม่ deadlock จนกว่าจะเกิดวงจริง

การป้องกัน deadlock

วิธีรายละเอียดตัดเงื่อนไข
Exploit or limit program behaviorจำกัดไม่ให้โปรแกรมทำอะไรที่อาจนำไปสู่ deadlock–
Provide enough resources"ต้องมีตะเกียบกี่ข้างจึงจะพอ"ข้อ 1
Eliminate wait while holdingrelease lock ก่อนเรียกออกนอก module · Banker's algorithmข้อ 3
Eliminate circular waitinglock ordering: acquire lock ตามลำดับที่ตายตัวเสมอ · resource allocation graphข้อ 4
Predict the futureถ้ารู้ว่าโปรแกรมจะทำอะไร ก็บอกได้ว่าการให้ resource จะนำไปสู่ deadlock หรือไม่ — ต้องอาศัยประสบการณ์–

ทำไม lock ordering ได้ผล

ถ้าทุก thread acquire ตามหมายเลขจากน้อยไปมาก thread ที่ถือ lock หมายเลข k จะรอได้เฉพาะ lock ที่หมายเลข มากกว่า k ไล่ตามเส้นการรอ หมายเลขจึงเพิ่มขึ้นเรื่อย ๆ และวนกลับมาที่เดิมไม่ได้ จึงไม่มีวง

กับ Dining Lawyers: ให้ตะเกียบมีหมายเลข และทุกคนหยิบข้างที่หมายเลขน้อยกว่าก่อน คนสุดท้ายในวงจะหยิบ "ซ้ายก่อน" ต่างจากคนอื่น วงจึงขาด

"ต้องมีตะเกียบกี่ข้างจึงจะพอ"

Detection และ Recovery

แทนที่จะป้องกัน ปล่อยให้เกิดแล้วค่อยแก้ก็ได้

Detection

  • ตรวจหา cycle ใน Resource Allocation Graph
  • ใช้ Wait-For Graph algorithm

Recovery

  • Thread rollback: ย้อนหรือยกเลิกการ lock.acquire
  • Thread restarting: เริ่ม thread ที่ติด deadlock ใหม่

Resource Allocation Graph

  • วงกลม = thread · สี่เหลี่ยม = resource
  • เส้นจาก resource ไป thread = thread ถือ resource นั้น
  • เส้นจาก thread ไป resource = thread ขอและรอ resource นั้น
  • ถ้า resource แต่ละชนิดมีชิ้นเดียว (เช่น lock): มี cycle ⇔ มี deadlock

Wait-for graph คือกราฟเดียวกันที่ตัด resource ออก เหลือแต่เส้น thread → thread ("A รอ B") หา cycle ได้ตรงกว่า

ตัวอย่างไล่ทีละขั้น

โจทย์: thread A ถือ L1 และขอ L2 · thread B ถือ L2 และขอ L3 · thread C ถือ L3 มี deadlock ไหม แล้วถ้า C ขอ L1 ล่ะ

  1. วาดเส้น: L1 → A, A → L2, L2 → B, B → L3, L3 → C
  2. ไล่ตามเส้นจาก A: A → L2 → B → L3 → C แล้วจบที่ C (C ไม่ได้รออะไร)
  3. ไม่มี cycle → ไม่ deadlock C ทำงานต่อ release L3 → B ได้ L3 ทำต่อ release L2 → A ได้ L2
  4. ถ้า C ขอ L1: เพิ่มเส้น C → L1 → ไล่ได้ A → L2 → B → L3 → C → L1 → A กลับมาที่เดิม
  5. มี cycle → deadlock ทั้งสาม thread
  6. wait-for graph: A → B → C → A

ตรวจสี่เงื่อนไข: lock มีจำกัด ✓, ยึดคืนไม่ได้ ✓, ทุกตัวถือหนึ่งรออีกหนึ่ง ✓, เป็นวง ✓

จุดที่มักพลาด

1. คิดว่า starvation กับ deadlock คือสิ่งเดียวกัน

deadlock ⇒ starvation แต่ starvation ไม่จำเป็นต้องเป็น deadlock

2. คิดว่ามีแค่บางข้อจากสี่ข้อก็ deadlock ได้

ต้องครบทั้งสี่ การป้องกันจึงตัดเพียงข้อเดียวก็พอ

3. คิดว่า acquire ลำดับเดียวกันแล้วปลอดภัยเสมอ

ตัวอย่างสอง lock กับ condition variable ยัง deadlock ได้ เพราะ wait ปล่อยแค่ lock ที่ส่งให้มัน

4. วาดทิศเส้นกลับด้าน

resource → thread คือ ถืออยู่ · thread → resource คือ รออยู่

5. คิดว่ารันแล้วไม่ค้างแปลว่าไม่มี deadlock

deadlock ขึ้นกับ schedule เหมือน race condition

6. คิดว่า lock เป็น preemptable resource

lock เป็น non-preemptable ยึดคืนกลางทางแล้ว shared data อาจค้างในสภาพไม่สมบูรณ์

ที่มา: 05-Synchronization-part4-Conclusion_v2_1.pdf หน้า 14–21