Livelock (การล็อกแบบแอคทีฟ) คือสถานการณ์ในการเขียนโปรแกรมแบบมัลติเธรดที่เธรดไม่ได้ถูกบล็อกแต่ตอบสนองต่อการกระทำของกันและกันอย่างไม่รู้จบโดยไม่ทำงานที่เป็นประโยชน์ ตาม Baeldung (Java Concurrency Guide, 2024) ใน Livelock เธรดจะเปลี่ยนสถานะอย่างต่อเนื่องเพื่อตอบสนองต่อสถานะของเธรดข้างเคียง แต่ไม่มีเธรดใดบรรลุเป้าหมายของมัน แตกต่างจาก Deadlock Livelock ใช้ CPU 100% ซึ่งทำให้แบตเตอรี่ของอุปกรณ์มือถือหมดอย่างรวดเร็ว
ประเด็นสำคัญ
Livelock (การล็อกแบบแอคทีฟ) คือสถานการณ์ในระบบมัลติเธรดที่เธรดไม่ได้ถูกบล็อกแต่ก็ไม่ได้ทำงานที่เป็นประโยชน์เช่นกัน แต่ละเธรดตรวจจับว่ามันไม่สามารถดำเนินการต่อได้และพยายามแก้ไข แต่การกระทำของมันกระตุ้นปฏิกิริยาเดียวกันในเธรดอื่น ๆ ผลลัพธ์คือระบบสลับระหว่างสถานะอย่างไม่รู้จบโดยไม่คืบหน้า
การเปรียบเทียบคลาสสิกของ Livelock คือคนสองคนพบกันในทางเดินแคบ แต่ละคนพยายามหลบไปด้านข้างเพื่อให้อีกคนผ่านไป แต่ทั้งคู่ทำการเคลื่อนไหวเดียวกันพร้อมกันและกลับมายืนเผชิญหน้ากันอีกครั้ง พวกเขา ไม่ได้ยืนนิ่ง (นั่นจะเป็น Deadlock) แต่กำลังเคลื่อนไหวอย่างแอคทีฟ แต่ไม่มีทางผ่านกันไปได้ ในการเขียนโปรแกรม สิ่งนี้สอดคล้องกับเธรดที่ปล่อยและรับทรัพยากรซ้ำแล้วซ้ำเล่า
ในการพัฒนาโมบาย Livelock เป็นอันตรายอย่างยิ่งเพราะมันไม่มีใครสังเกตเห็น: แอปไม่ค้าง อินเทอร์เฟซไม่ถูกบล็อก แต่ แบตเตอรี่หมด เร็วขึ้น 2-3 เท่าเนื่องจากโหลด CPU 100% จากเธรดพื้นหลัง ตามการทดสอบของ Google (Android Battery Optimization, 2023) Livelock ใน Service พื้นหลังสามารถลดอายุการใช้งานแบตเตอรี่ของอุปกรณ์ได้ถึง 40%
Livelock เกิดขึ้นเมื่อเธรดหลายตัวใช้ กลยุทธ์การตอบสนองต่อความขัดแย้งแบบเดียวกัน หากเธรด A ไม่สามารถรับทรัพยากรและปล่อยทรัพยากรปัจจุบันของมัน ในขณะที่เธรด B ทำสิ่งเดียวกันพร้อมกัน ทั้งคู่จะทำซ้ำวงจร — และสถานการณ์จะเกิดขึ้นซ้ำอย่างไม่รู้จบ ซึ่งเป็นลักษณะเฉพาะของอัลกอริทึมที่ใช้ TryLock และการปล่อยอัตโนมัติเมื่อล้มเหลว
เมื่อเธรดใช้ ความล่าช้าคงที่ ก่อนลองใหม่ พวกมันอาจเข้าสู่วงจรซิงโครนัส หากเธรดทั้งสองรอเวลาเท่ากัน พวกมันจะพยายามรับทรัพยากรอีกครั้งพร้อมกันและปล่อยมันอีกครั้งพร้อมกัน ปัญหาแก้ไขได้โดยใช้ exponential backoff กับองค์ประกอบสุ่ม (jitter) เช่นเดียวกับในอัลกอริทึม CSMA/CD ใน Ethernet
ในการพัฒนาโมบาย Livelock มักเกิดขึ้นเนื่องจาก การใช้งานคิวงานที่ไม่ถูกต้อง ตัวอย่างเช่น เมื่อเธรดทำงานประมวลผลข้อความเสร็จแต่เนื่องจากตรรกะการจัดลำดับความสำคัญ ส่งต่อการควบคุมให้กับเธรดทำงานอื่นที่ทำสิ่งเดียวกันอย่างต่อเนื่อง สถานการณ์เหล่านี้เป็นเรื่องปกติสำหรับ ThreadPoolExecutor ที่กำหนดเองด้วยนโยบาย RejectedExecutionHandler ที่ไม่ได้มาตรฐาน
พิจารณาสถานการณ์ที่เธรดสองตัวใช้ TryLock และปล่อยทรัพยากรเมื่อล้มเหลว การล็อกแบบแอคทีฟ เกิดขึ้นเพราะเธรดทั้งสองใช้ตรรกะเดียวกันและลองใหม่แบบซิงโครนัส
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.TimeUnit
class LivelockWorker(private val name: String,
private val lock1: ReentrantLock,
private val lock2: ReentrantLock) {
fun execute() {
while (true) {
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
println("$name — เสร็จสิ้น!")
lock2.unlock()
lock1.unlock()
return
} else {
lock1.unlock() // ปล่อยและลองใหม่
}
}
Thread.sleep(50) // ความล่าช้าเดียวกัน — ปัจจัยหลักของ Livelock
}
}
}
หากอินสแตนซ์สองตัวของ LivelockWorker ถูกเรียกใช้ด้วยลำดับการรับ lock1 และ lock2 ที่ต่างกัน พวกมันจะเข้าสู่การล็อกแบบแอคทีฟ แต่ละตัวจะรับทรัพยากรแรก ไม่ได้รับทรัพยากรที่สอง ปล่อยทรัพยากรแรก รอ 50 ms และลองใหม่ — อย่างไม่รู้จบ ใช้ CPU การแก้ไขคือเพิ่มองค์ประกอบสุ่มในความล่าช้า (jitter) และจำกัดจำนวนครั้งในการลองใหม่
เวอร์ชันที่แก้ไขแล้วใช้ exponential backoff พร้อม jitter แบบสุ่ม หลังจากแต่ละครั้งที่ล้มเหลว เวลารอจะเพิ่มขึ้นด้วยตัวคูณสุ่ม ซึ่งทำลายการซิงโครไนซ์ระหว่างเธรด
fun executeWithBackoff() {
var delay = 10L
var attempts = 0
while (attempts < 5) {
if (lock1.tryLock(delay, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(delay, TimeUnit.MILLISECONDS)) {
println("สำเร็จ!")
lock2.unlock(); lock1.unlock()
return
}
lock1.unlock()
}
delay = (delay * 2 + (0..50).random())
attempts++
}
println("ล้มเหลวหลังจาก 5 ครั้ง")
}
แม้จะมีความคล้ายคลึงภายนอก Livelock และ Deadlock มีกลไกและผลลัพธ์ที่แตกต่างกันโดยพื้นฐาน ใน Deadlock เธรดถูกบล็อกและไม่ใช้ CPU — แอปพลิเคชันเพียงแค่ค้าง ใน Livelock เธรดทำงานอยู่ ใช้ CPU 100% แต่ไม่ทำงานที่เป็นประโยชน์ การเลือกกลยุทธ์การแก้ไขขึ้นอยู่กับการระบุประเภทการล็อกที่ถูกต้อง
| พารามิเตอร์ | Deadlock | Livelock |
|---|---|---|
| สถานะเธรด | BLOCKED / WAITING | RUNNABLE |
| การใช้ CPU | น้อยที่สุด | สูง (90-100%) |
| การใช้แบตเตอรี่ | ต่ำ | สูง |
| การตรวจจับ | Thread Dump | CPU Profiler + การวิเคราะห์ด้วยภาพ |
| สาเหตุทั่วไป | ลำดับการรับล็อกที่ต่างกัน | กลยุทธ์ตอบสนองต่อความขัดแย้งแบบเดียวกัน |
| การแก้ไข | ลำดับชั้นของล็อก | ขีดจำกัดการลองใหม่ + exponential backoff |
ในการพัฒนาโมบาย ความแตกต่างในทางปฏิบัตินั้นมหาศาล Deadlock นำไปสู่ ANR และการรีสตาร์ทแอป — มันถูกตรวจจับและรายงานผ่าน Google Play Console Livelock ไม่มีใครสังเกตเห็น: แอปดูเหมือนทำงาน แต่แบตเตอรี่หมดภายในหนึ่งชั่วโมง และผู้ใช้เพียงแค่ลบแอปทิ้ง ตาม Firebase Analytics (App Retention Report, 2024) ผู้ใช้ 68% ลบแอปหากมันใช้แบตเตอรี่มากเกินไปในพื้นหลัง
การตรวจจับ Livelock ยากกว่า Deadlock เพราะระบบไม่ส่งสัญญาณชัดเจน — ไม่มีข้อยกเว้น ไม่มี ANR ไม่มีข้อความแสดงข้อผิดพลาด วิธีการวินิจฉัยหลักคือ CPU Profiler ใน Android Studio หากเธรดอยู่ในสถานะ RUNNABLE อย่างต่อเนื่องแต่ไม่ได้ดำเนินการ I/O หรือการคำนวณที่เป็นประโยชน์ — นั่นคือข้อสงสัยว่าเป็น Livelock
ตัวบ่งชี้เพิ่มเติมคือ การใช้แบตเตอรี่ผิดปกติ เมื่อแอปไม่ได้ทำงาน Android Battery Historian (เครื่องมือจาก Android SDK) สร้างกราฟการใช้พลังงานตามส่วนประกอบ หาก CPU Wakelock ถูกยึดไว้โดยไม่มีเหตุผลชัดเจน — ให้รัน Method Tracing และวิเคราะห์สแต็กการเรียกของเธรดที่น่าสงสัย
ในระดับโค้ด การบันทึกการลองใหม่ พร้อม threadId และการประทับเวลาช่วยได้ หากบันทึกแสดงการลองใหม่หลายพันครั้งต่อวินาทีโดยไม่มีความสำเร็จแม้แต่ครั้งเดียว — นั่นคือ Livelock ขอแนะนำให้นำ circuit breaker แบบ Hystrix หรือตัวนับการลองใหม่ที่มีเกณฑ์มาใช้ ซึ่งเมื่อเกินจะปิดการทำงานและแจ้งให้นักพัฒนาทราบผ่าน Crashlytics
วิธีที่ง่ายและน่าเชื่อถือที่สุดคือ จำกัดจำนวนครั้ง ในการรับทรัพยากร หากการดำเนินการล้มเหลวหลังจาก N ครั้ง เธรดจะย้ายไปยังสถานะข้อผิดพลาดและแจ้งให้ผู้ใช้ทราบ N ถูกเลือกโดยการทดลอง: สำหรับแอปบนมือถือ โดยทั่วไปคือ 3-5 ครั้ง สิ่งนี้กำจัด Livelock ไม่รู้จบอย่างสมบูรณ์ ด้วยค่าใช้จ่ายของการแจ้งเตือนผิดพลาดที่หาได้ยากภายใต้โหลดสูง
แทนที่จะใช้ความล่าช้าคงที่ระหว่างการลอง จะใช้ การหยุดชั่วคราวที่เพิ่มขึ้นแบบเอกซ์โปเนนเชียล พร้อมองค์ประกอบสุ่ม สูตร: delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter) วิธีการนี้ไม่เพียงทำลายการซิงโครไนซ์ของเธรด แต่ยังลดภาระโดยรวมของระบบภายใต้การแข่งขันสูง ใช้ในอัลกอริทึมโปรโตคอลเครือข่ายและแนะนำโดย Google สำหรับตรรกะการลองใหม่ของ Firebase Realtime Database
การกำหนด กลยุทธ์ที่แตกต่าง ให้กับเธรดที่แตกต่างกันกำจัดสาเหตุหลักของ Livelock — การตอบสนองต่อความขัดแย้งที่เหมือนกัน ตัวอย่างเช่น เธรดที่มีลำดับความสำคัญสูงรับทรัพยากรโดยไม่ปล่อย ในขณะที่เธรดที่มีลำดับความสำคัญต่ำปล่อยและรอ ในการพัฒนาโมบาย เธรด UI สามารถมีลำดับความสำคัญในการรับล็อก ในขณะที่เธรดทำงานพื้นหลังใช้ TryLock พร้อมหมดเวลา
ในสถาปัตยกรรมบางอย่าง Livelock ถูกป้องกันในระดับการออกแบบ: ปล่อยทรัพยากรในทิศทางเดียวเท่านั้น ตัวอย่างเช่น หากเธรด A ส่งต่อการควบคุมไปยังเธรด B ผ่านช่องทางคงที่เสมอ และ B ไม่เคยพยายามส่งคืนการควบคุมไปยัง A — วงจรปฏิกิริยาเป็นไปไม่ได้ สถาปัตยกรรมไปป์ไลน์ที่มีขั้นตอนการประมวลผลทางเดียวกำจัด Livelock ระหว่างขั้นตอนที่อยู่ติดกันใน Android CameraX และ MediaPipe อย่างสมบูรณ์
คำถามที่พบบ่อย
ลูปไม่รู้จบ ไม่ขึ้นอยู่กับปัจจัยภายนอกและทำซ้ำการดำเนินการเดียวโดยไม่โต้ตอบกับเธรดอื่น Livelock คือปฏิกิริยาต่อการกระทำของเธรดอื่นเสมอ: เธรดเปลี่ยนพฤติกรรมเพื่อตอบสนองต่อสถานะของเธรดข้างเคียง สร้างวงจรป้อนกลับแบบปิด Thread Dump ในกรณีของ Livelock แสดงการสลับบริบทอย่างต่อเนื่อง
ในฐานข้อมูล Livelock เกิดขึ้นเมื่อ ธุรกรรมถูกเลื่อนออกไปอย่างต่อเนื่อง เนื่องจากการล็อกโดยธุรกรรมอื่น ตัวอย่างเช่น DBMS ใช้อัลกอริทึม wait-die: หากธุรกรรมที่มีเวลาเริ่มต้นน้อยกว่าขัดแย้งกับธุรกรรมใหม่กว่า มันจะย้อนกลับและเริ่มต้นใหม่ แต่ทุกครั้งก็เจอความขัดแย้งเดียวกัน แก้ไขได้ด้วยการหน่วงเวลาเริ่มต้นใหม่แบบสุ่ม
ในบางระบบ Livelock ดีกว่า Deadlock เพราะเธรดยังคงทำงานและสามารถตรวจจับปัญหาได้ ตัวอย่างเช่น ในอัลกอริทึมการล็อกแบบมองโลกในแง่ดี (optimistic locking) พฤติกรรมคล้าย livelock เป็นที่ยอมรับได้ตราบเท่าที่ขีดจำกัดการลองใหม่รับประกันความสำเร็จในที่สุด นี่คือการแลกเปลี่ยนระหว่างประสิทธิภาพและการรับประกันความคืบหน้า
Livelock ยากมากที่จะทำซ้ำในการทดสอบ เพราะมันต้องการการจับคู่เวลาที่แม่นยำของเธรด การทดสอบหน่วยทำงานแบบกำหนดขึ้นและแทบไม่พบการล็อกแบบแอคทีฟ แนะนำให้ใช้การทดสอบความเครียดด้วยการเรียกใช้ซ้ำหลายครั้งภายใต้โหลดและการตรวจสอบการใช้ CPU ในโปรไฟเลอร์
บนเซิร์ฟเวอร์ Livelock นำไปสู่ ประสิทธิภาพลดลงและหมดเวลา แต่เซิร์ฟเวอร์สามารถขยายขนาดในแนวนอนได้ บน Android Livelock ทำให้แบตเตอรี่หมดและอุปกรณ์ร้อนเกินไป สร้างประสบการณ์ผู้ใช้ที่แย่ที่สุด นอกจากนี้ อุปกรณ์มือถือมีแกน CPU จำนวนจำกัด ดังนั้น Livelock จึงนำไปสู่การไม่สามารถทำงานของทั้งระบบได้เร็วกว่า
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม