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.
نظام التشغيل لا يمكنه سحب القفل قسرًا من الخيط. يتم تحرير المورد فقط عندما يحرره الخيط بنفسه. في بعض الأنظمة (مثل SQLite WAL mode)، يتم تطبيق الاستباق القسري على مستوى العمليات الفردية، مما يقلل من خطر 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، الخيط غير محظور — إنه جاهز للتنفيذ لكنه لا يحصل على وقت المعالج. في Android، السيناريو النموذجي هو خيط خلفية منخفض الأولوية لا يتم تنفيذه أبدًا إذا كان خيط UI وخيوط Service نشطة باستمرار.
Livelock (القفل النشط) هو حالة حيث الخيوط غير محظورة لكنها تتفاعل إلى ما لا نهاية مع تصرفات بعضها البعض دون القيام بعمل مفيد. التشبيه الكلاسيكي — شخصان يلتقيان في ممر وكلاهما يحاول إفساح الطريق، متحركين في نفس الاتجاه. على عكس Deadlock، الخيوط في Livelock تستهلك وحدة المعالجة المركزية، مما يستنزف بطارية الجهاز.
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 بين العمليات عند استخدام أقفال الملفات أو الإشارات بين العمليات.
في coroutines، يحدث Deadlock على مستوى الدوال المعلقة (suspend) ولا يحظر خيط نظام التشغيل، مما يجعله أقل وضوحًا. Mutex من kotlinx.coroutines هو قفل معلق (suspending) — لا يحظر الخيط، لكن coroutine لا يتم تنفيذها. للكشف، استخدم 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. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا