Livelock (القفل النشط) هو حالة في البرمجة متعددة الخيوط حيث الخيوط غير محظورة ولكنها تتفاعل بلا نهاية مع تصرفات بعضها البعض دون أداء عمل مفيد. وفقًا لـ Baeldung (Java Concurrency Guide, 2024)، في Livelock تغير الخيوط حالتها باستمرار استجابة لحالة الخيوط المجاورة، لكن لا يحقق أي منها هدفه. على عكس Deadlock، Livelock يستهلك 100% من المعالج، مما يستنزف بطارية الجهاز المحمول بسرعة.
النقاط الرئيسية
Livelock (القفل النشط) هو حالة في نظام متعدد الخيوط حيث الخيوط غير محظورة ولكنها أيضًا لا تؤدي عملاً مفيدًا. يكتشف كل خيط أنه لا يمكنه الاستمرار ويحاول إصلاح ذلك، لكن أفعاله تثير نفس رد الفعل لدى الخيوط الأخرى. ونتيجة لذلك، ينتقل النظام بلا نهاية بين الحالات دون إحراز تقدم.
التشبيه الكلاسيكي لـ Livelock هو شخصان يلتقيان في ممر ضيق. يحاول كل منهما التنحي جانبًا ليفسح الطريق للآخر، لكن كلاهما يقوم بنفس الحركة في نفس الوقت ويجدان نفسيهما وجهًا لوجه مرة أخرى. إنهما لا يقفان ساكنين (سيكون ذلك Deadlock)، بل يتحركان بنشاط، لكنهما لا يستطيعان الابتعاد عن بعضهما. في البرمجة، هذا يتوافق مع خيوط تطلق الموارد وتستعيدها باستمرار.
في التطوير المحمول، Livelock خطير بشكل خاص لأنه يمر دون أن يلاحظه المستخدم: التطبيق لا يتجمد، الواجهة لا تُحظر، ولكن البطارية تستنزف أسرع 2-3 مرات بسبب تحميل المعالج بنسبة 100% من الخيوط الخلفية. وفقًا لاختبارات Google (Android Battery Optimization, 2023)، يمكن أن يقلل Livelock في Service الخلفي من عمر بطارية الجهاز بنسبة 40%.
ينشأ Livelock عندما تستخدم عدة خيوط نفس استراتيجية رد الفعل للنزاع. إذا لم يتمكن الخيط A من الحصول على مورد وأطلق موارده الحالية، بينما يفعل الخيط B نفس الشيء في نفس الوقت، يكرر كلاهما الدورة — ويتكرر الموقف بلا نهاية. هذا مميز بشكل خاص للخوارزميات التي تستخدم TryLock مع التحرير التلقائي عند الفشل.
عندما تستخدم الخيوط تأخيرًا ثابتًا قبل إعادة المحاولة، يمكن أن تدخل في دورة متزامنة. إذا انتظر كلا الخيطين نفس المدة الزمنية، سيحاولان مرة أخرى في نفس الوقت الحصول على المورد ويحررانه في نفس الوقت مرة أخرى. تُحل المشكلة باستخدام exponential backoff مع مكون عشوائي (jitter)، كما في خوارزمية CSMA/CD في Ethernet.
في التطوير المحمول، ينشأ Livelock غالبًا بسبب تنفيذ غير صحيح لقوائم المهام. على سبيل المثال، عندما ينهي خيط عامل معالجة رسالة ولكن، بسبب منطق تحديد الأولويات، ينقل التحكم باستمرار إلى خيط عامل آخر يفعل نفس الشيء. هذه المواقف نموذجية لـ ThreadPoolExecutors المخصصة مع سياسات 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 مللي ثانية، ويعيدان المحاولة — بلا نهاية، مستهلكين المعالج. الحل هو إضافة مكون عشوائي إلى التأخير (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، الخيوط محظورة ولا تستهلك المعالج — يتجمد التطبيق ببساطة. في Livelock، الخيوط نشطة، تستهلك 100% من المعالج، لكنها لا تؤدي عملًا مفيدًا. يعتمد اختيار استراتيجية الحل على التحديد الصحيح لنوع القفل.
| المعامل | Deadlock | Livelock |
|---|---|---|
| حالة الخيوط | BLOCKED / WAITING | RUNNABLE |
| استهلاك المعالج | أدنى | مرتفع (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 لكنه لا يؤدي عمليات إدخال/إخراج أو حسابات مفيدة — فهذا اشتباه في 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 ينقل التحكم دائمًا إلى الخيط B عبر قناة ثابتة، و B لا يحاول أبدًا إعادة التحكم إلى A — دورة التفاعل مستحيلة. بنية pipeline مع مراحل معالجة أحادية الاتجاه تزيل تمامًا Livelock بين المراحل المتجاورة في Android CameraX و MediaPipe.
الأسئلة الشائعة
الحلقة اللانهائية لا تعتمد على عوامل خارجية وتكرر عملية واحدة دون التفاعل مع الخيوط الأخرى. Livelock هو دائمًا رد فعل على تصرفات الخيوط الأخرى: يغير الخيط سلوكه استجابة لحالة الخيوط المجاورة، مما يخلق حلقة تغذية راجعة مغلقة. يُظهر Thread Dump في حالة Livelock تبديلًا مستمرًا للسياق.
في قواعد البيانات، ينشأ Livelock عندما يتم تأجيل المعاملة باستمرار بسبب أقفال المعاملات الأخرى. على سبيل المثال، يستخدم نظام إدارة قواعد البيانات خوارزمية wait-die: إذا تعارضت معاملة ذات وقت بداية أقل مع معاملة أحدث، يتم التراجع عنها وإعادة تشغيلها، لكنها تواجه نفس النزاع في كل مرة. يُحل ذلك باستخدام تأخير إعادة تشغيل عشوائي.
في بعض الأنظمة، يكون Livelock مفضلًا على Deadlock لأن الخيوط تبقى نشطة ويمكنها اكتشاف المشكلة. على سبيل المثال، في خوارزميات القفل التفاؤلي (optimistic locking)، يُسمح بسلوك مشابه لـ livelock إذا كان حد إعادة المحاولة يضمن الإنهاء النهائي. هذا حل وسط بين الأداء وضمان التقدم.
Livelock صعب جدًا إعادة إنتاجه في الاختبارات لأنه يتطلب تطابقًا دقيقًا لتوقيتات الخيوط. تُنفذ اختبارات الوحدة بشكل حتمي ونادرًا ما تكشف القفل النشط. يُوصى باستخدام stress testing مع تشغيل متكرر تحت الحمل ومراقبة استهلاك المعالج في المُحلل.
على الخادم، يؤدي Livelock إلى تدهور الأداء وانتهاء المهلات، لكن الخادم يتوسع أفقيًا. على Android، يستنزف Livelock البطارية ويسخن الجهاز، مما يخلق أسوأ تجربة مستخدم. بالإضافة إلى ذلك، تحتوي الأجهزة المحمولة على عدد محدود من أنوية المعالج، لذلك يؤدي Livelock بسرعة أكبر إلى عدم قابلية النظام للعمل.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا