Deadlock في تطوير التطبيقات المحمولة: ما هو، أسباب الحدوث وطرق تجنب الحظر المتبادل

المؤلف: IT Sectr نُشر: 2026-03-18 وقت القراءة: 10 دق

Deadlock (الحظر المتبادل) هو حالة ينتظر فيها خيطان أو أكثر إلى أجل غير مسمى تحرير الموارد المحتجزة من قبل مشاركين آخرين. وفقًا لـ Oracle Java Tutorials (2024)، يحدث Deadlock عند الانتظار الدائري عندما يحمل كل خيط قفلاً يحتاجه خيط آخر. بدون أدوات كشف خاصة، Deadlock يوقف تنفيذ التطبيق بالكامل بدون أخطاء مرئية.

أهم النقاط

  • Deadlock — حظر متبادل للخيوث حيث ينتظر كل خيط موردًا محتجزًا من قبل خيط آخر
  • شروط Coffman الأربعة (Mutual Exclusion، Hold and Wait، No Preemption، Circular Wait) ضرورية لحدوث Deadlock
  • Deadlock يختلف عن Starvation في أن الخيوط ليست محظورة بل تنتظر بنشاط في تبعية دائرية
  • Thread Dump — الأداة الرئيسية لاكتشاف Deadlock في JVM و Android Runtime
  • تسلسل الأقفال وترتيب موحد لالتقاط الموارد — الطريقة الرئيسية لمنع الحظر المتبادل

ما هو Deadlock؟

Deadlock هو حالة في البرمجة متعددة الخيوط حيث يقوم خيطان أو أكثر بحظر بعضهما البعض بشكل دائم. كل خيط يحمل موردًا يحتاجه خيط آخر ولا يحرره أثناء انتظار الحصول على المورد المفقود. ونتيجة لذلك، لا يمكن لأي من الخيوط مواصلة التنفيذ.

في تطوير التطبيقات المحمولة، يعتبر Deadlock خطيرًا بشكل خاص لأنه لا يسبب استثناءات أو أعطالًا. التطبيق ببساطة يتوقف عن الاستجابة لإجراءات المستخدم (ANR — Application Not Responding)، والحل الوحيد هو إنهاء العملية قسرًا. وفقًا لـ Google (Android Performance Patterns، 2023)، حوالي 15% من تقارير ANR في Google Play Console مرتبطة بالحظر المتبادل في الخيوط الخلفية.

الفرق الرئيسي بين Deadlock ومشاكل التزامن الأخرى هو عدم قابليته للعكس دون تدخل خارجي. لن تحرر الخيوط الموارد بنفسها لأن جدولة نظام التشغيل لا يمكنها سحب القفل قسرًا. هذا يميز Deadlock عن Livelock، حيث تكون الخيوط نشطة ولكنها لا تقوم بعمل مفيد.

شروط حدوث Deadlock

في عام 1971، صاغ Edward G. Coffman أربعة شروط إلزامية ضرورية لحدوث Deadlock. إذا كان أحدها على الأقل غائبًا، فالحظر المتبادل مستحيل. تُعرف هذه الشروط بشروط Coffman وتشكل أساس جميع خوارزميات منع Deadlock.

الاستبعاد المتبادل (Mutual Exclusion)

يمكن التقاط المورد بواسطة خيط واحد فقط في كل لحظة زمنية. إذا كان المورد يسمح بالقراءة المتزامنة بواسطة عدة خيوط (مثل ReadWriteLock في وضع القراءة)، فلن يحدث Deadlock. هذا الشرط ينبع من طبيعة Mutex والأقفال.

الاحتجاز والانتظار (Hold and Wait)

الخيط يحتفظ بمورد تم التقاطه بالفعل وينتظر في نفس الوقت التقاط مورد آخر. إذا كان الخيط يمكنه تحرير المورد الحالي قبل طلب التالي (من خلال القفل على مرحلتين)، يتم كسر شرط Hold and Wait. في Android، يتجلى هذا غالبًا عندما يحمل الخيط قفل قاعدة بيانات ويحاول التقاط قفل SharedPreferences.

غياب الاستباق القسري (No Preemption)

نظام التشغيل لا يمكنه سحب القفل قسرًا من الخيط. يتم تحرير المورد فقط عندما يحرره الخيط بنفسه. في بعض الأنظمة (مثل SQLite WAL mode)، يتم تطبيق الاستباق القسري على مستوى العمليات الفردية، مما يقلل من خطر Deadlock.

الانتظار الدائري (Circular Wait)

يوجد سلسلة مغلقة من الخيوط، كل منها ينتظر موردًا محتجزًا من قبل التالي في السلسلة. على سبيل المثال، الخيط A يحمل المورد 1 وينتظر المورد 2، الخيط B يحمل المورد 2 وينتظر المورد 1. هذا هو الشرط الوحيد الذي يمكن للمطور إزالته معماريًا — من خلال تسلسل الأقفال. إذا التقطت جميع الخيوط الموارد بترتيب عالمي محدد بدقة، فإن الدورة مستحيلة فيزيائيًا.

في الممارسة العملية، في تطبيقات Android، يحدث Deadlock غالبًا بسبب التقاطع الضمني للأقفال على مستويات مختلفة: قفل قاعدة البيانات (Room)، قفل SharedPreferences، وقفل المجموعة في الذاكرة. كل من هذه الأقفال يُدار بواسطة مكونات مختلفة، وبدون بروتوكول مركزي لترتيب الالتقاط، ينشئ المطورون دورات عن غير قصد.

مثال Deadlock في كود Kotlin

لنفكر في مثال كلاسيكي للحظر المتبادل — خيطان يلتقطان الأقفال بترتيب مختلف. إذا قام الخيط الأول بقفل المورد A وحاول التقاط B، والثاني بقفل B وحاول التقاط A، يحدث Deadlock.

kotlin
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 — خمول نشط. فهم الاختلافات أمر بالغ الأهمية لاختيار استراتيجية الحل الصحيحة.

الخاصيةDeadlockStarvationLivelock
حالة الخيوطمحظورة (BLOCKED)جاهزة (RUNNABLE)نشطة (RUNNABLE)
تنفيذ العمللالانعم، لكنه عديم الفائدة
السببانتظار دائريجدولة غير عادلةمعالجة غير صحيحة للتعارض
الاكتشافThread Dump، مهلاتمراقبة التقدمعداد إعادة المحاولة

Starvation (التجويع) يحدث عندما يؤجل جدول المهام باستمرار تنفيذ خيط منخفض الأولوية لصالح الآخرين. على عكس Deadlock، الخيط غير محظور — إنه جاهز للتنفيذ لكنه لا يحصل على وقت المعالج. في Android، السيناريو النموذجي هو خيط خلفية منخفض الأولوية لا يتم تنفيذه أبدًا إذا كان خيط UI وخيوط Service نشطة باستمرار.

Livelock (القفل النشط) هو حالة حيث الخيوط غير محظورة لكنها تتفاعل إلى ما لا نهاية مع تصرفات بعضها البعض دون القيام بعمل مفيد. التشبيه الكلاسيكي — شخصان يلتقيان في ممر وكلاهما يحاول إفساح الطريق، متحركين في نفس الاتجاه. على عكس Deadlock، الخيوط في Livelock تستهلك وحدة المعالجة المركزية، مما يستنزف بطارية الجهاز.

كيفية اكتشاف Deadlock

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.

طرق منع الحظر المتبادل

تسلسل الأقفال (Lock Ordering)

الطريقة الأكثر موثوقية هي إنشاء ترتيب عالمي لالتقاط الأقفال في جميع أنحاء التطبيق. إذا التقطت جميع الخيوط دائمًا القفل ذو الرقم الأصغر أولاً، ثم الرقم الأكبر، فإن الانتظار الدائري (شرط Circular Wait) مستحيل. في المشاريع الكبيرة، يتم توثيق الترتيب والتحقق منه من خلال مراجعة الكود.

TryLock مع مهلة

TryLock هو طريقة قفل لا تحظر الخيط إلى أجل غير مسمى، بل تعيد false إذا لم يتم الحصول على القفل خلال وقت محدد. في Java، يتم تنفيذ ذلك عبر ReentrantLock.tryLock(timeout, TimeUnit)، في Kotlin Coroutines — عبر Mutex.withLock مع مهلة. عند الفشل، يحرر الخيط جميع الموارد الملتقطة ويعيد المحاولة لاحقًا.

خوارزمية الصراف (Banker's Algorithm)

خوارزمية الصراف هي طريقة نظرية لمنع Deadlock اقترحها Edsger Dijkstra. تمثل تخصيص الموارد كمعاملات بنكية: النظام لا يخصص موردًا إذا كان ذلك قد يؤدي إلى حالة غير آمنة (deadlock). في الممارسة، نادرًا ما تُستخدم الخوارزمية في تطوير التطبيقات المحمولة بسبب صعوبة معرفة الاحتياجات القصوى للخيوط مسبقًا، لكن مبادئها تُستخدم في قواعد بيانات SQLite وأنظمة الملفات.

الأسئلة المتكررة

هل يمكن أن يحدث Deadlock في تطبيق أحادي الخيط؟

لا، للحظر المتبادل يلزم خيطان على الأقل. في الكود أحادي الخيط، يتم تنفيذ جميع العمليات بالتسلسل، لذلك الانتظار الدائري مستحيل. ومع ذلك، يمكن أن يحدث Deadlock بين العمليات عند استخدام أقفال الملفات أو الإشارات بين العمليات.

كيف يختلف Deadlock في Kotlin Coroutines عن Deadlock في الخيوط؟

في coroutines، يحدث Deadlock على مستوى الدوال المعلقة (suspend) ولا يحظر خيط نظام التشغيل، مما يجعله أقل وضوحًا. Mutex من kotlinx.coroutines هو قفل معلق (suspending) — لا يحظر الخيط، لكن coroutine لا يتم تنفيذها. للكشف، استخدم DebugProbes من وحدة kotlinx-coroutines-debug.

ما هو Deadlock في SQLite على Android؟

Deadlock في SQLite يحدث عندما تحاول اتصالين بقاعدة البيانات تنفيذ معاملات بترتيبات مختلفة. يكتشف SQLite مثل هذه الحالات ويعيد رمز الخطأ SQLITE_BUSY أو SQLITE_LOCKED. في Android، يُوصى باستخدام Room مع مثيل واحد لقاعدة البيانات والمعاملات عبر @Transaction، مما يلغي Deadlock بين الاتصالات.

كيف يكتشف Android Deadlock؟

Android Runtime لديه كاشف Deadlock مدمج يعمل عند توليد ANR (Application Not Responding). يحلل النظام Thread Dump لجميع خيوط التطبيق ويحدد الحظر المتبادل. النتيجة متاحة في /data/anr/traces.txt وفي Google Play Console في قسم ANR Reports.

ماذا تفعل إذا تم العثور على Deadlock في الإنتاج؟

أولاً، احصل على Thread Dump لجميع خيوط التطبيق. حلل أي الأقفال يحملها كل خيط وأيها يحاول التقاطها. طبق مؤقت Watchdog مع تفريغ تلقائي عند تجاوز حد الوقت. بعد الإصلاح، أضف قاعدة lint ThreadSafety في خط أنابيب CI لمنع التكرار.

الخلاصة

  • Deadlock — حظر متبادل حيث تنتظر الخيوط إلى ما لا نهاية موارد محتجزة من قبل بعضها البعض
  • شروط Coffman الأربعة (Mutual Exclusion، Hold and Wait، No Preemption، Circular Wait) ضرورية لحدوث Deadlock
  • Thread Dump — الطريقة القياسية لكشف الحظر المتبادل في JVM و Android Runtime
  • تسلسل الأقفال بترتيب عالمي واحد يزيل تمامًا شرط الانتظار الدائري
  • TryLock مع مهلة يمنع الانتظار اللانهائي ويسمح للخيط بمعالجة عدم توفر المورد بشكل صحيح
  • Deadlock مقابل Starvation — في Deadlock الخيوط محظورة، في Starvation جاهزة للتنفيذ لكنها لا تحصل على CPU
  • مؤقتات Watchdog والمحللات الثابتة (ThreadSafe، Checker Framework) — حماية أساسية من Deadlock في CI/CD

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا