Deadlock (การล็อกซึ่งกันและกัน) เป็นสถานะที่เธรดตั้งแต่สองเธรดขึ้นไปรออย่างไม่มีที่สิ้นสุดเพื่อให้ทรัพยากรที่ถูกยึดครองโดยผู้เข้าร่วมอื่น ๆ ถูกปล่อยออกมา ตาม Oracle Java Tutorials (2024) Deadlock เกิดขึ้นในการรอแบบวงกลมเมื่อแต่ละเธรดถือล็อกที่จำเป็นสำหรับอีกเธรดหนึ่ง หากไม่มีเครื่องมือตรวจจับพิเศษ Deadlock จะหยุดการทำงานของแอปพลิเคชันโดยสมบูรณ์โดยไม่มีข้อผิดพลาดที่มองเห็นได้
ประเด็นสำคัญ
Deadlock คือสถานการณ์ในการเขียนโปรแกรมแบบหลายเธรดที่เธรดตั้งแต่สองเธรดขึ้นไปบล็อกซึ่งกันและกันอย่างถาวร แต่ละเธรดถือทรัพยากรที่จำเป็นสำหรับอีกเธรดหนึ่งและไม่ปล่อยมันในขณะที่รอครอบครองทรัพยากรที่ขาดหายไป ผลลัพธ์คือไม่มีเธรดใดสามารถดำเนินการต่อได้
ในการพัฒนามือถือ Deadlock มีความสำคัญเป็นพิเศษเพราะมันไม่ทำให้เกิดข้อยกเว้นหรือแครช แอปพลิเคชันเพียงแค่หยุดตอบสนองต่อการกระทำของผู้ใช้ (ANR — Application Not Responding) และทางออกเดียวคือการบังคับสิ้นสุดกระบวนการ ตามข้อมูลของ Google (Android Performance Patterns, 2023) ประมาณ 15% ของรายงาน ANR ใน Google Play Console เกี่ยวข้องกับการล็อกซึ่งกันและกันในเธรดพื้นหลัง
ความแตกต่างหลักระหว่าง Deadlock และปัญหาความพร้อมกันอื่น ๆ คือการไม่สามารถย้อนกลับได้โดยไม่มีการแทรกแซงจากภายนอก เธรดจะไม่ปล่อยทรัพยากรด้วยตนเองเพราะตัวจัดตารางเวลาของระบบปฏิบัติการไม่สามารถเพิกถอนล็อกได้โดยบังคับ สิ่งนี้แยก Deadlock ออกจาก Livelock ซึ่งเธรดทำงานอยู่แต่ไม่ได้ทำงานที่มีประโยชน์
ในปี 1971 Edward G. Coffman ได้กำหนดเงื่อนไขบังคับสี่ประการที่จำเป็นต่อการเกิด Deadlock หากขาดอย่างน้อยหนึ่งข้อ การล็อกซึ่งกันและกันเป็นไปไม่ได้ เงื่อนไขเหล่านี้รู้จักกันในชื่อเงื่อนไขของ Coffman และเป็นพื้นฐานของอัลกอริทึมการป้องกัน Deadlock ทั้งหมด
ทรัพยากรสามารถถูกครอบครองโดย เพียงเธรดเดียว ในแต่ละช่วงเวลา หากทรัพยากรอนุญาตให้อ่านพร้อมกันโดยหลายเธรด (เช่น ReadWriteLock ในโหมดอ่าน) Deadlock จะไม่เกิดขึ้น เงื่อนไขนี้เกิดจากธรรมชาติของ Mutex และล็อก
เธรดถือทรัพยากรที่ครอบครองอยู่แล้วและรอ ครอบครองทรัพยากรอื่น พร้อมกัน หากเธรดสามารถปล่อยทรัพยากรปัจจุบันก่อนขอทรัพยากรถัดไป (ผ่านการล็อกสองเฟส) เงื่อนไข Hold and Wait จะถูกทำลาย ใน Android สิ่งนี้มักปรากฏเมื่อเธรดถือล็อกฐานข้อมูลและพยายามครอบครองล็อก SharedPreferences
ระบบปฏิบัติการ ไม่สามารถแย่งล็อกไปจากเธรดโดยบังคับ ได้ ทรัพยากรจะถูกปล่อยเมื่อเธรดปล่อยมันเองเท่านั้น ในบางระบบ (เช่น โหมด WAL ของ SQLite) การแย่งชิงโดยบังคับถูกนำไปใช้ในระดับการดำเนินการแต่ละรายการ ซึ่งลดความเสี่ยงของ Deadlock
มี ห่วงโซ่ปิดของเธรด ซึ่งแต่ละเธรดรอทรัพยากรที่ถูกถือโดยเธรดถัดไปในห่วงโซ่ ตัวอย่างเช่น เธรด A ถือทรัพยากร 1 และรอทรัพยากร 2 เธรด B ถือทรัพยากร 2 และรอทรัพยากร 1 นี่เป็นเงื่อนไขเดียวที่นักพัฒนาสามารถกำจัดได้ในเชิงสถาปัตยกรรม — ผ่านลำดับชั้นล็อก หากเธรดทั้งหมดครอบครองทรัพยากรในลำดับสากลที่กำหนดอย่างเคร่งครัด วงจรจะเป็นไปไม่ได้ในทางกายภาพ
ในทางปฏิบัติ ในแอปพลิเคชัน Android Deadlock เกิดขึ้นบ่อยที่สุดเนื่องจาก การตัดกันโดยนัย ของล็อกในระดับต่าง ๆ: ล็อกฐานข้อมูล (Room), ล็อก SharedPreferences และล็อกคอลเลกชันในหน่วยความจำ ล็อกแต่ละตัวถูกจัดการโดยส่วนประกอบต่าง ๆ และหากไม่มีโปรโตคอลรวมศูนย์สำหรับลำดับการครอบครอง นักพัฒนาจะสร้างวงจรโดยไม่ตั้งใจ
ลองพิจารณาตัวอย่างคลาสสิกของการล็อกซึ่งกันและกัน — สองเธรดครอบครองล็อกในลำดับที่ต่างกัน หากเธรดแรกล็อกทรัพยากร A และพยายามครอบครอง B และเธรดที่สองล็อก B และพยายามครอบครอง A Deadlock จะเกิดขึ้น
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // การจำลองการทำงาน
synchronized(lockB) {
println("operationA เสร็จสมบูรณ์")
}
}
}
fun operationB() {
synchronized(lockB) { // ลำดับย้อนกลับ
Thread.sleep(50)
synchronized(lockA) {
println("operationB เสร็จสมบูรณ์")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// แอปพลิเคชันจะค้างตลอดไป — Deadlock!
}
ในตัวอย่างนี้ operationA ครอบครอง lockA และ operationB ครอบครอง lockB จากนั้นแต่ละเธรดพยายามครอบครองล็อกที่สอง — และทั้งคู่รออย่างไม่มีกำหนด โปรแกรมค้างโดยไม่มีข้อความแสดงข้อผิดพลาด วิธีแก้ไขเพียงอย่างเดียวคือรับประกันลำดับการครอบครองล็อกเดียวกันในทุกเมธอด
ปัญหาความพร้อมกันทั้งสามนี้มักถูกสับสน แต่กลไกและผลลัพธ์ของมันแตกต่างกันโดยพื้นฐาน Deadlock — การหยุดโดยสมบูรณ์, Starvation — การรอทรัพยากรอย่างไม่มีที่สิ้นสุด, Livelock — การไม่ทำงานอย่างแข็งขัน การเข้าใจความแตกต่างเป็นสิ่งสำคัญอย่างยิ่งในการเลือกกลยุทธ์การแก้ไขที่ถูกต้อง
| ลักษณะ | Deadlock | Starvation | Livelock |
|---|---|---|---|
| สถานะเธรด | ถูกบล็อก (BLOCKED) | พร้อม (RUNNABLE) | ทำงาน (RUNNABLE) |
| การทำงาน | ไม่ | ไม่ | ใช่ แต่ไร้ประโยชน์ |
| สาเหตุ | การรอแบบวงกลม | การจัดตารางเวลาไม่เป็นธรรม | การจัดการข้อขัดแย้งไม่ถูกต้อง |
| การตรวจจับ | Thread Dump, หมดเวลา | การติดตามความคืบหน้า | ตัวนับลองใหม่ |
Starvation (การอดอยาก) เกิดขึ้นเมื่อตัวจัดตารางเวลาเลื่อนการทำงานของเธรดที่มีลำดับความสำคัญต่ำอย่างต่อเนื่องเพื่อให้เธรดอื่น แตกต่างจาก Deadlock ตรงที่เธรด ไม่ได้ถูกบล็อก — มันพร้อมทำงานแต่ไม่ได้รับเวลา CPU ใน Android สถานการณ์ทั่วไปคือเธรดพื้นหลังที่มีลำดับความสำคัญต่ำไม่เคยทำงานถ้าเธรด UI และเธรด Service ทำงานอยู่ตลอดเวลา
Livelock (การล็อกเชิงรุก) เป็นสถานการณ์ที่เธรดไม่ได้ถูกบล็อกแต่ ตอบสนองต่อการกระทำของกันและกันอย่างไม่มีที่สิ้นสุด โดยไม่ทำงานที่มีประโยชน์ การเปรียบเทียบคลาสสิก — คนสองคนพบกันในทางเดินและทั้งคู่พยายามหลีกทาง เคลื่อนที่ไปในทิศทางเดียวกัน แตกต่างจาก Deadlock ตรงที่เธรดใน Livelock ใช้ CPU ทำให้แบตเตอรี่อุปกรณ์หมด
Thread Dump เป็นเครื่องมือหลักในการตรวจจับการล็อกซึ่งกันและกันใน JVM และ Android Runtime ระหว่างการดัมพ์ JVM จะวิเคราะห์กราฟการพึ่งพาระหว่างมอนิเตอร์โดยอัตโนมัติและทำเครื่องหมายวงจร Deadlock ใน Android Studio สามารถรับเธรดดัมพ์ได้ผ่าน Android Profiler หรือคำสั่ง kill -3 PID จาก ADB Shell
การตรวจจับ Deadlock อัตโนมัติขณะรันไทม์ถูกนำไปใช้ผ่าน ตัวจับเวลา Watchdog หากเธรดไม่ดำเนินการเสร็จสิ้นภายในเวลาที่กำหนด watchdog จะเริ่มการดัมพ์และส่งรายงานไปยังระบบ Crash Reporting (Firebase Crashlytics, Sentry) ตามข้อมูลของ Sentry (Issue Resolution Report, 2024) การกำหนดค่า watchdog ช่วยลดเวลาการวินิจฉัย Deadlock จากสัปดาห์เหลือเพียงไม่กี่ชั่วโมง
ในขั้นตอนการพัฒนา เครื่องมือวิเคราะห์แบบคงที่ ThreadSafe ของ JetBrains และ Checker Framework พร้อมโมดูล Lock Checker มีประสิทธิภาพ เครื่องมือเหล่านี้วิเคราะห์ลำดับการครอบครองล็อกในระดับซอร์สโค้ดและเตือนเกี่ยวกับวงจรที่อาจเกิดขึ้น นอกจากนี้ แนะนำให้ใช้ Test-Driven Deadlock Detection — การทดสอบความเครียดที่ดำเนินการกับลำดับล็อกต่าง ๆ ในหลายร้อยเธรด
ที่ควรให้ความสนใจเป็นพิเศษคือ Cooperative Deadlock Detection — วิธีการที่เธรดแลกเปลี่ยนข้อมูลเกี่ยวกับล็อกที่ครอบครองผ่านรีจิสทรีส่วนกลาง หากเธรดตรวจพบวงจรที่อาจเกิดขึ้น มันจะปล่อยทรัพยากรทั้งหมดและลองดำเนินการอีกครั้ง วิธีการนี้ใช้ในระบบกระจาย (Apache ZooKeeper, Google Chubby) และกำลังค่อย ๆ ถูกนำมาใช้ในการพัฒนามือถือผ่านไลบรารีเช่น Jetpack Sync
วิธีที่น่าเชื่อถือที่สุดคือการสร้าง ลำดับการครอบครองล็อกในระดับสากล ทั่วทั้งแอปพลิเคชัน หากเธรดทั้งหมดครอบครองล็อกที่มีหมายเลขน้อยกว่าก่อนเสมอ ตามด้วยล็อกที่มีหมายเลขมากกว่า การรอแบบวงกลม (เงื่อนไข Circular Wait) จะเป็นไปไม่ได้ ในโครงการขนาดใหญ่ ลำดับจะถูกบันทึกเป็นเอกสารและตรวจสอบผ่านการตรวจสอบโค้ด
TryLock เป็นวิธีการล็อกที่ไม่บล็อกเธรดอย่างไม่มีกำหนด แต่ส่งคืน false หากไม่สามารถครอบครองล็อกได้ภายในเวลาที่กำหนด ใน Java สิ่งนี้ถูกนำไปใช้ผ่าน ReentrantLock.tryLock(timeout, TimeUnit) ใน Kotlin Coroutines — ผ่าน Mutex.withLock พร้อมหมดเวลา เมื่อล้มเหลว เธรดจะปล่อยทรัพยากรที่ครอบครองทั้งหมดและลองอีกครั้งในภายหลัง
อัลกอริทึมของนายธนาคาร เป็นวิธีการทางทฤษฎีในการป้องกัน Deadlock ที่เสนอโดย Edsger Dijkstra มันจำลองการจัดสรรทรัพยากรเป็นธุรกรรมธนาคาร: ระบบจะไม่จัดสรรทรัพยากรถ้ามันอาจนำไปสู่สถานะที่ไม่ปลอดภัย (deadlock) ในทางปฏิบัติ อัลกอริทึมไม่ค่อยได้ใช้ในการพัฒนามือถือเนื่องจากความยากในการทราบความต้องการสูงสุดของเธรดล่วงหน้า แต่หลักการของมันถูกใช้ในฐานข้อมูล SQLite และระบบไฟล์
คำถามที่พบบ่อย
ไม่ การล็อกซึ่งกันและกันต้องการอย่างน้อยสองเธรด ในโค้ดเธรดเดียว การดำเนินการทั้งหมดจะทำงานตามลำดับ ดังนั้นการรอแบบวงกลมจึงเป็นไปไม่ได้ อย่างไรก็ตาม Deadlock สามารถเกิดขึ้นระหว่างกระบวนการเมื่อใช้ล็อกไฟล์หรือเซมาฟอร์ระหว่างกระบวนการ
ในคอรูทีน Deadlock เกิดขึ้นในระดับ ฟังก์ชันระงับ (suspend) และไม่บล็อกเธรดของระบบปฏิบัติการ ทำให้สังเกตเห็นได้น้อยกว่า Mutex จาก kotlinx.coroutines เป็นล็อกแบบระงับ (suspending) — มันไม่บล็อกเธรด แต่คอรูทีนจะไม่ทำงาน สำหรับการตรวจจับ ให้ใช้ DebugProbes จากโมดูล kotlinx-coroutines-debug
Deadlock ใน SQLite เกิดขึ้นเมื่อสองการเชื่อมต่อฐานข้อมูลพยายามดำเนินการธุรกรรมในลำดับที่ต่างกัน SQLite ตรวจจับสถานการณ์ดังกล่าวและส่งคืนรหัสข้อผิดพลาด SQLITE_BUSY หรือ SQLITE_LOCKED ใน Android แนะนำให้ใช้ Room พร้อมอินสแตนซ์ฐานข้อมูลเดียวและธุรกรรมผ่าน @Transaction ซึ่งกำจัด Deadlock ระหว่างการเชื่อมต่อ
Android Runtime มีตัวตรวจจับ Deadlock ในตัวที่ทำงานเมื่อเกิด ANR (Application Not Responding) ระบบจะวิเคราะห์ Thread Dump ของเธรดทั้งหมดในแอปพลิเคชันและทำเครื่องหมายการล็อกซึ่งกันและกัน ผลลัพธ์สามารถดูได้ที่ /data/anr/traces.txt และใน Google Play Console ส่วน ANR Reports
ขั้นแรก รับ Thread Dump ของเธรดทั้งหมดในแอปพลิเคชัน วิเคราะห์ว่าแต่ละเธรดถือล็อกอะไรและพยายามครอบครองล็อกอะไร นำตัวจับเวลา Watchdog มาใช้พร้อมการดัมพ์อัตโนมัติเมื่อเกินเวลาที่กำหนด หลังจากแก้ไขแล้ว ให้เพิ่มกฎ lint ThreadSafety ในไปป์ไลน์ CI เพื่อป้องกันการเกิดซ้ำ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม