Starvation في التطبيقات المحمولة — الجوهر والأسباب وطرق منع تجويع الخيط

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

Starvation (تجويع الخيط) — هي حالة لا يتمكن فيها الخيط من الوصول إلى مورد ضروري لمواصلة العمل، على الرغم من أنه جاهز للتنفيذ. وفقًا لـ Baeldung (Java Thread Starvation, 2024)، ينشأ التجويع بسبب جدولة غير عادلة، حيث يتم تأجيل الخيوط ذات الأولوية المنخفضة باستمرار لصالح الخيوط ذات الأولوية الأعلى. على عكس Deadlock، Starvation لا يمنع الخيط — بل يظل في حالة RUNNABLE ولكنه لا يحصل أبدًا على وقت وحدة المعالجة المركزية.

النقاط الرئيسية

  • Starvation — حالة لا يتمكن فيها الخيط من الوصول إلى مورد على الرغم من كونه جاهزًا للتنفيذ
  • على عكس Deadlock، يظل الخيط المجوع في حالة RUNNABLE —ليس محظورًا، لكنه لا يتقدم
  • الجدولة غير العادلة (على سبيل المثال، المزامنة عبر synchronized) — السبب الرئيسي لـ Starvation على JVM
  • Fair Lock (ReentrantLock(true)) يضمن ترتيب FIFO عادلًا للوصول إلى القفل
  • Thread Priority في تطوير التطبيقات المحمولة يُوصى بعدم تغييرها — Android Runtime تدير الأولويات بنفسها

ما هو Starvation؟

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

في تطوير التطبيقات المحمولة، يظهر Starvation كـ تنفيذ غير متساوٍ للمهام: بعض العمليات تُنفذ فورًا، بينما تعاني أخرى من تأخيرات كارثية. على سبيل المثال، قد لا يتمكن خيط مزامنة البيانات في الخلفية من الوصول إلى قاعدة البيانات أبدًا إذا كان خيط واجهة المستخدم ومعالجات الرسوم المتحركة يسبقونه باستمرار. وفقًا لـ Android Developer Blog (Performance Matters, 2023)، حوالي 12% من الإطارات المفقودة (jank) على Android ناتجة عن Starvation للمهام الخلفية التي يعتمد عليها العرض.

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

أسباب تجويع الخيط

الأقفال غير العادلة (Non-Fair Locks)

synchronized في Java و Kotlin هو مثال كلاسيكي على آلية غير عادلة. تحت التنافس العالي، يمكن لـ JVM أن تمنح القفل باستمرار لنفس الخيوط النشطة، بينما تخسر الخيوط الأخرى السباق باستمرار. هذا ليس خطأ في JVM بل مقايضة في التصميم: الأقفال غير العادلة توفر إنتاجية أعلى على حساب عدالة الوصول. بالنسبة للتطبيقات المحمولة التي تحتوي على 4–8 خيوط، هذه المشكلة ذات صلة خاصة.

الاستخدام غير الصحيح للأولويات

تعيين أولويات مختلفة للخيوط يمكن أن يؤدي إلى Starvation للخيوط ذات الأولوية المنخفضة. في Android Runtime، يوزع مجدول CFS (Completely Fair Scheduler) لنظام Linux وقت وحدة المعالجة المركزية بشكل متناسب مع الأولويات، وإذا كانت الخيوط عالية الأولوية نشطة باستمرار، فقد لا تحصل الخيوط منخفضة الأولوية على وقت CPU أبدًا. تمنع Google بشدة تغيير أولويات الخيوط في Android — النظام يديرها بنفسه.

الأقسام الحرجة الطويلة

إذا احتفظ خيط بقفل لفترة طويلة جدًا (أداء عمليات حسابية ثقيلة أو طلبات شبكة أو عمليات ملفات داخل كتلة synchronized)، فإن الخيوط الأخرى التي تنتظر هذا القفل تعاني من التجويع. هذا خطير بشكل خاص في Android، حيث تسبب العمليات الطويلة في خيط واجهة المستخدم ANR، ونقلها إلى خيوط خلفية دون تحسين الأقسام الحرجة ينقل مشكلة Starvation إلى خيوط العامل.

مثال على Starvation في كود Kotlin

تأمل مثالًا حيث يكتسب خيط قفلًا بشكل متكرر جدًا بسبب جدولة غير عادلة. يتم توضيح Starvation من خلال حلقة لا نهائية لخيط عالي الأولوية يمنع خيطًا منخفض الأولوية من الوصول إلى مورد مشترك.

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id حصل على الوصول")
            Thread.sleep(10)  // محاكاة العمل
        }
    }
}

fun main() {
    val resource = SharedResource()

    // خيط عالي الأولوية — نشط باستمرار
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // خيط منخفض الأولوية — قد لا يحصل على الوصول أبدًا
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // "Low" قد لا يطبع رسالة أبدًا — Starvation!
}

في هذا المثال، خيط highPriority يكتسب القفل باستمرار ويحرره لمدة 10 مللي ثانية فقط. بسبب الطبيعة غير العادلة لـ synchronized، من المحتمل جدًا أن يمنح مجدول JVM القفل مرة أخرى لنفس الخيط الذي حرره للتو — خيط الأولوية المنخفضة يعاني من التجويع. الحل هو استخدام ReentrantLock(true) مع علامة fair، التي تضمن ترتيب FIFO للانتظار.

النسخة المصححة بقفل عادل تضمن توزيعًا منصفًا للوصول إلى المورد.

kotlin
class FairSharedResource {
    private val fairLock = ReentrantLock(true)  // fair = true

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id حصل على الوصول (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

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

المعلمةStarvationDeadlockLivelock
حالة الخيطRUNNABLEBLOCKEDRUNNABLE
التقدملا يوجدلا يوجدلا يوجد (رغم النشاط)
استخدام CPUمنخفضأدنىمرتفع (حتى 100%)
السببجدولة غير عادلةانتظار دورينفس الاستجابة للتعارض
الإصلاح الرئيسيFair Lock، تقصير الأقسام الحرجةتسلسل هرمي للأقفالحد إعادة المحاولة، exponential backoff

يُعتبر Starvation أقل خطورة من Deadlock لأنه ليس مميتًا — تحت الحمل المنخفض، سيتم تنفيذ الخيط المجوع في النهاية. ومع ذلك، في الاستخدام الفعلي لـ Android، حيث الذاكرة و CPU محدودان، يمكن أن يستمر Starvation لدقائق، مما يخلق تجربة مستخدم غير مقبولة.

كيفية اكتشاف Starvation

Thread Dump المأخوذ بشكل متكرر على فترات قصيرة — الطريقة الأساسية لاكتشاف التجويع. إذا كان الخيط باستمرار في حالة RUNNABLE لكن مكدس الاستدعاءات الخاص به لا يتغير عبر عدة تفريغات — فهذه علامة كلاسيكية على Starvation. في Android Studio، استخدم Android Profiler مع تسجيل حالة الخيوط بمرور الوقت.

الاكتشاف التلقائي ممكن من خلال مراقبة وقت التنفيذ للمهام. إذا كانت مهمة ذات وقت تنفيذ متوقع (مثل 50 مللي ثانية) تستغرق 5 ثوانٍ أو أكثر — فهناك احتمال كبير لوجود Starvation. في التطبيقات المحمولة، يتيح Firebase Performance Monitoring إعداد تتبعات مخصصة للأقسام الحرجة وتلقي إشعارات عند تجاوز الحدود.

لتشخيص Starvation الناجم عن كتل synchronized، استخدم Java Flight Recorder (JFR) (متوفر على Android عبر OpenJDK API) أو Async Profiler. تُظهر هذه الأدوات أي الشاشات لديها أعلى أوقات الانتظار وأي الخيوط تتنافس على كل شاشة. تتكامل بيانات JFR مع IntelliJ IDEA Ultimate من خلال الملف الشخصي المدمج.

طرق منع تجويع الخيط

Fair Lock (ReentrantLock مع علامة true)

ReentrantLock(true) يضمن أن الخيوط تحصل على القفل بترتيب FIFO. على عكس synchronized، لا يسمح القفل العادل للخيط الذي حرر القفل للتو بإعادة اكتسابه فورًا. هذا يزيل Starvation تمامًا، على الرغم من أنه يقلل الإنتاجية الإجمالية بنسبة 10–20% بسبب الحمل الإضافي لصيانة قائمة الانتظار.

الهياكل الذرية بدون أقفال

هياكل البيانات بدون أقفال (ConcurrentHashMap، AtomicReference، LongAdder) تزيل Starvation بحكم تعريفها، حيث لا تحتوي على أقفال يمكن الاحتفاظ بها بواسطة خيط واحد. تستخدم جميع العمليات تعليمات CAS لوحدة المعالجة المركزية التي تضمن تقدم خيط واحد على الأقل في عدد محدود من الخطوات. لتطوير التطبيقات المحمولة، يُفضل استخدام ConcurrentLinkedQueue لقوائم المهام.

الأقسام الحرجة القصيرة

تقليل وقت الاحتفاظ بالقفل — طريقة عالمية لتقليل خطر Starvation. انقل العمليات الثقيلة (الشبكة، الإدخال/الإخراج للقرص، الحسابات المعقدة) خارج كتل synchronized. استخدم ReadWriteLock للسيناريوهات حيث لا يجب أن يعاني القراء من التجويع بسبب الكتاب النادرين. توفر مكتبة Kotlin Coroutines Mutex مع آلية تعليق لا تمنع خيط نظام التشغيل.

متغيرات الشرط والإشارات

Condition.await() و signal() يجب استخدامها بحذر: خيط ينتظر على Condition يستيقظ مع خيوط أخرى (استيقاظ زائف)، وتتنافس جميعًا على القفل. إذا عاد خيط فورًا إلى الانتظار بعد await بينما تمكنت خيوط أخرى من الحصول على القفل، فقد يستيقظ الخيط المجوع ويعود للنوم إلى أجل غير مسمى. تحقق دائمًا من الشرط في حلقة while بدلاً من if لضمان إعادة التحقق.

الأسئلة الشائعة

ما الفرق بين Starvation وعكس الأولوية (Priority Inversion)؟

Priority Inversion — هي حالة يحتفظ فيها خيط منخفض الأولوية بقفل يحتاجه خيط عالي الأولوية. نتيجة لذلك، ينتظر الخيط عالي الأولوية الخيط منخفض الأولوية — تنعكس الأولويات. Starvation مشكلة أوسع: لا يمكن للخيط الحصول على مورد بغض النظر عن الأولوية، بسبب جدولة غير عادلة أو أقسام حرجة طويلة.

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

لا، Starvation مشكلة تعدد خيوط. في الكود أحادي الخيط لا يوجد تنافس على الموارد أو جدولة خيوط. ومع ذلك، يمكن أن يحدث Starvation في كود غير متزامن أحادي الخيط (مثل حلقة الأحداث في JavaScript) إذا قامت مهمة صغيرة بتأجيل تنفيذ المهام الأخرى إلى أجل غير مسمى عبر setTimeout بتأخير صفري.

كيف يرتبط نموذج ذاكرة Java بـ Starvation؟

JMM (Java Memory Model) يحدد قواعد رؤية التغييرات بين الخيوط لكنه لا يضمن جدولة عادلة. synchronized، وفقًا لـ JMM، يضمن تناسقًا تسلسليًا — صحة أساسية — لكنه لا يمنع Starvation. العدالة تتطلب آليات إضافية غير محددة في JMM.

ما هو Starvation في خيط واجهة المستخدم Android؟

خيط واجهة المستخدم (Main Thread) لا يمكن أن يعاني من التجويع بالمعنى الكلاسيكي لأنه يتمتع بأعلى أولوية. ومع ذلك، يحدث Starvation عندما ينتظر خيط واجهة المستخدم نتيجة من خيط خلفي مجوع. سيناريو نموذجي: AsyncTask أو كوروتين تحمل بيانات لكنها لا تستطيع الوصول إلى قاعدة البيانات بسبب التنافس مع خيوط أخرى، وتتجمد واجهة المستخدم أثناء الانتظار.

كيف نمنع Starvation في Kotlin Coroutines؟

في الكوروتينات، لمنع Starvation، استخدم limitedParallelism على Dispatchers.IO لتجنب استنفاد الخيوط. للمزامنة، استخدم Mutex من kotlinx.coroutines.sync — يعلق الكوروتين بدلاً من منع الخيط، مما يقلل من خطر التجويع. تجنب runBlocking في الكوروتينات، لأنه يمكن أن يلتقط خيط المجمع ويسبب Starvation لكوروتينات أخرى.

الملخص

  • Starvation — حالة يكون فيها الخيط جاهزًا للتنفيذ لكنه لا يستطيع الحصول على مورد بسبب جدولة غير عادلة
  • على عكس Deadlock، يظل الخيط المجوع في حالة RUNNABLE ويمكن أن يُنفذ عندما ينخفض الحمل
  • الأقفال غير العادلة (synchronized) والاستخدام غير الصحيح للأولويات — الأسباب الرئيسية لـ Starvation
  • Fair Lock (ReentrantLock مع علامة true) يضمن ترتيب وصول FIFO ويزيل التجويع تمامًا
  • الهياكل بدون أقفال (ConcurrentHashMap، AtomicReference) تزيل Starvation على مستوى البنية
  • Thread Dump مع التقاطات متكررة و Java Flight Recorder — طرق فعالة لتشخيص Starvation
  • الأقسام الحرجة القصيرة و ReadWriteLock تقلل احتمالية التجويع في الأنظمة عالية الحمل

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

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

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

اقرأ أيضًا