Livelock (قفل فعال) — وضعیتی در برنامهنویسی چندنخی است که نخها قفل نشدهاند، اما بینهایت به اقدامات یکدیگر واکنش نشان میدهند بدون اینکه کار مفیدی انجام دهند. به گفته Baeldung (Java Concurrency Guide, 2024)، در Livelock نخها دائماً در پاسخ به وضعیت نخهای مجاور تغییر وضعیت میدهند، اما هیچکدام به هدف نمیرسند. برخلاف Deadlock، Livelock 100% CPU مصرف میکند که باتری دستگاه موبایل را به سرعت تخلیه میکند.
نکات اصلی
Livelock (قفل فعال) — وضعیتی در سیستم چندنخی است که نخها قفل نشدهاند اما کار مفیدی هم انجام نمیدهند. هر نخ تشخیص میدهد که نمیتواند کار را ادامه دهد و سعی میکند آن را اصلاح کند، اما اقدامات او همان واکنش را در سایر نخها ایجاد میکند. در نتیجه سیستم بینهایت بین وضعیتها جابهجا میشود بدون اینکه پیشرفتی حاصل شود.
قیاس کلاسیک Livelock — دو نفر در راهروی باریک با هم روبرو میشوند. هرکدام سعی میکند راه را باز کند و به کناری میرود، اما هر دو همزمان همان حرکت را انجام میدهند و دوباره روبروی هم قرار میگیرند. آنها ایستاده نیستند (این Deadlock بود)، بلکه فعالانه حرکت میکنند اما هنوز نمیتوانند از هم عبور کنند. در برنامهنویسی، این معادل نخهایی است که دائماً منابع را آزاد کرده و دوباره تصاحب میکنند.
در توسعه موبایل، Livelock بهویژه خطرناک است زیرا برای کاربر نامرئی است: برنامه هنگ نمیکند، رابط کاربری مسدود نمیشود، اما باتری به دلیل 100% بار CPU توسط نخهای پسزمینه 2-3 برابر سریعتر تخلیه میشود. بر اساس آزمایشهای Google (Android Battery Optimization, 2023)، Livelock در سرویس پسزمینه میتواند عمر باتری دستگاه را 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 میلیثانیه صبر میکند و تکرار میکند — بینهایت، با مصرف 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 نخها فعال هستند، 100% CPU مصرف میکنند اما کار مفیدی انجام نمیدهند. انتخاب استراتژی رفع بستگی به تشخیص صحیح نوع قفل دارد.
| پارامتر | Deadlock | Livelock |
|---|---|---|
| وضعیت نخها | BLOCKED / WAITING | RUNNABLE |
| مصرف CPU | حداقل | زیاد (90-100%) |
| مصرف باتری | کم | زیاد |
| تشخیص | Thread Dump | CPU Profiler + تحلیل بصری |
| علت معمول | ترتیب متفاوت تصاحب قفلها | استراتژی یکسان واکنش به تضاد |
| رفع | سلسلهمراتب قفلها | Retry limit + exponential backoff |
در توسعه موبایل، تفاوت عملی بسیار زیاد است. Deadlock منجر به ANR و راهاندازی مجدد برنامه میشود — از طریق Google Play Console تشخیص داده و گزارش میشود. Livelock نادیده گرفته میشود: برنامه به نظر کار میکند، اما باتری در یک ساعت تخلیه میشود و کاربر به سادگی برنامه را حذف میکند. طبق Firebase Analytics (App Retention Report, 2024)، 68% کاربران برنامه را حذف میکنند اگر در پسزمینه بیش از حد باتری مصرف کند.
تشخیص Livelock از Deadlock دشوارتر است زیرا سیستم سیگنالهای آشکاری نمیدهد — استثنا وجود ندارد، ANR وجود ندارد، پیام خطایی وجود ندارد. روش اصلی تشخیص — CPU Profiler در Android Studio. اگر نخ دائماً در وضعیت RUNNABLE باشد اما عملیات ورودی-خروجی یا محاسبات مفیدی انجام ندهد — این مشکوک به Livelock است.
نشانه اضافی — مصرف غیرعادی باتری در زمان بیکاری برنامه. Android Battery Historian (ابزاری از Android SDK) نمودارهای مصرف انرژی را بر اساس مؤلفهها میسازد. اگر CPU Wakelock بدون دلیل ظاهری نگه داشته شود — باید Method Tracing را اجرا کرده و پشته فراخوانی نخهای مشکوک را تحلیل کرد.
در سطح کد، ثبت تلاشهای مجدد با threadId و زمان کمک میکند. اگر لاگ هزاران تلاش مجدد در ثانیه را بدون هیچ موفقیتی نشان دهد — این Livelock است. توصیه میشود از circuit breaker مشابه Hystrix یا شمارنده retry با آستانه فعالسازی استفاده شود که پس از عبور از حد، عملیات را غیرفعال کرده و توسعهدهنده را از طریق Crashlytics مطلع میکند.
سادهترین و مطمئنترین روش — محدود کردن تعداد تلاشها برای تصاحب منبع. اگر پس از N تلاش عملیات موفق نشد، نخ به وضعیت خطا رفته و به کاربر اطلاع میدهد. N به صورت تجربی انتخاب میشود: برای برنامههای موبایل معمولاً 3-5 تلاش. این کار Livelock بینهایت را به قیمت فعالسازیهای نادر نادرست در بار بالا کاملاً حذف میکند.
به جای تأخیر ثابت بین تلاشها از مکث نمایی افزایشیابنده با مؤلفه تصادفی استفاده میشود. فرمول: delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter). این رویکرد نه تنها همزمانی نخها را از بین میبرد، بلکه بار کلی سیستم را در رقابت بالا کاهش میدهد. در الگوریتمهای پروتکلهای شبکه استفاده میشود و توسط Google برای منطق تلاش مجدد Firebase Realtime Database توصیه شده است.
اختصاص استراتژیهای متفاوت به نخهای مختلف علت اصلی Livelock — واکنش یکسان به تضاد — را از بین میبرد. مثلاً نخ با اولویت بالا منبع را بدون آزادسازی دریافت میکند و نخ با اولویت پایین آزاد کرده و منتظر میماند. در توسعه موبایل، نخ UI میتواند در تصاحب قفلها اولویت داشته باشد و نخهای کارگر پسزمینه از TryLock با تایماوت استفاده کنند.
در برخی معماریها، Livelock در سطح طراحی جلوگیری میشود: آزادسازی منابع فقط در یک جهت. مثلاً اگر نخ A همیشه کنترل را از طریق یک کانال ثابت (Channel) به نخ B منتقل کند و B هرگز سعی در بازگرداندن کنترل به A نداشته باشد — چرخه واکنش غیرممکن است. معماری خط لوله با مراحل پردازش یکطرفه در Android CameraX و MediaPipe کاملاً Livelock را بین مراحل مجاور حذف میکند.
سوالات متداول
حلقه بینهایت به عوامل خارجی وابسته نیست و یک عملیات را بدون تعامل با سایر نخها تکرار میکند. Livelock همیشه واکنشی به اقدامات سایر نخها است: نخ رفتار خود را در پاسخ به وضعیت نخهای مجاور تغییر میدهد و یک بازخورد بسته ایجاد میکند. Thread Dump در Livelock جابهجایی دائمی زمینه را نشان میدهد.
در پایگاه داده، Livelock زمانی ایجاد میشود که تراکنش دائماً به تعویق میافتد به دلیل قفلهای تراکنشهای دیگر. مثلاً DBMS از الگوریتم wait-die استفاده میکند: اگر تراکنش با زمان شروع کمتر با تراکنش جدیدتر تضاد داشته باشد، برگردانده و دوباره راهاندازی میشود، اما هر بار به همان تضاد برخورد میکند. با randomized restart delay حل میشود.
در برخی سیستمها Livelock از Deadlock ترجیحدادنیتر است زیرا نخها فعال میمانند و میتوانند مشکل را تشخیص دهند. مثلاً در الگوریتمهای قفل خوشبینانه (optimistic locking) رفتار مشابه Livelock در صورت تضمین retry limit مجاز است. این مصالحهای بین عملکرد و تضمین پیشرفت است.
Livelock در آزمایشها بسیار دشوار بازتولید میشود زیرا نیاز به تطابق دقیق زمانبندی نخها دارد. تستهای واحد به صورت قطعی اجرا میشوند و به ندرت قفل فعال را تشخیص میدهند. توصیه میشود از Stress Testing با اجرای مکرر تحت بار و نظارت بر مصرف CPU در پروفایلر استفاده شود.
در سرور، Livelock منجر به کاهش عملکرد و timeout میشود، اما سرور به صورت افقی مقیاسپذیر است. در Android، Livelock باتری را تخلیه کرده و دستگاه را داغ میکند و تجربه کاربری بدتری ایجاد میکند. علاوه بر این، در دستگاههای موبایل تعداد هستههای CPU محدود است، بنابراین Livelock سریعتر به عدم کارایی کل سیستم منجر میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید