Thread Pool في تطوير التطبيقات المحمولة — الأساسيات، تجمع الخيوط ومبدأ العمل

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

Thread Pool — هي آلية لإدارة الخيوط، حيث يتم إعادة استخدام تجمع خيوط تم إنشاؤه مسبقاً لتنفيذ المهام، مما يتجنب التكاليف الإضافية لإنشاء الخيوط وتدميرها. في تطوير التطبيقات المحمولة، يُستخدم thread pool للعمليات الخلفية: طلبات الشبكة، معالجة الصور، العمل مع قواعد البيانات. وفقاً لوثائق Google Android (2025)، فإن ExecutorService هو الطريقة الموصى بها لإدارة الخيوط الخلفية في Android. في iOS، يؤدي OperationQueue و GCD DispatchQueue مع قوائم الانتظار المتزامنة العامة دوراً مماثلاً.

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

  • Thread Pool — تجمع خيوط قابلة لإعادة الاستخدام لتنفيذ المهام الخلفية دون التكاليف الإضافية لإنشاء الخيوط.
  • ExecutorService في Android يدير التجمع عبر ThreadPoolExecutor بمعلمات قابلة للتكوين.
  • OperationQueue في iOS يغلف تجمع الخيوط من خلال maxConcurrentOperationCount.
  • Core pool size — الحد الأدنى لعدد الخيوط الجاهزة دائماً لتنفيذ المهام.
  • Work queue يخزن المهام التي تنتظر خيطاً متاحاً في التجمع.

ما هو Thread Pool؟

Thread Pool (تجمع الخيوط) — هو نمط معماري يتم فيه إنشاء عدد ثابت من الخيوط مسبقاً وإعادة استخدامها لتنفيذ مهام متعددة. بدلاً من إنشاء خيط جديد لكل عملية (وهو مكلف: حوالي 1 ميغابايت من المكدس لكل خيط في JVM)، توضع المهام في قائمة انتظار وتنفذ بواسطة الخيوط المتاحة من التجمع. في تطوير التطبيقات المحمولة، يعتبر thread pool ضرورياً للأداء — يحد Android و iOS من عدد الخيوط لكل تطبيق.

لماذا Thread Pool مهم في تطوير التطبيقات المحمولة

إنشاء خيط هو عملية مكلفة: تخصيص مكدس، تسجيل في النظام، تبديل السياق. على الأجهزة المحمولة ذات الموارد المحدودة، يؤدي الإنشاء غير المنضبط للخيوط إلى OOM (OutOfMemoryError) على Android والتقييد على iOS. Thread Pool يحل كلتا المشكلتين: يحد من أقصى عدد للخيوط التي تعمل في وقت واحد ويعيد استخدام الخيوط المنشأة بالفعل. توصي Google باستخدام ExecutorService بدلاً من raw Thread()، وتوصي Apple باستخدام OperationQueue بدلاً من Thread.

المعلمةبدون تجمع (raw Thread)مع Thread Pool
إنشاء الخيطلكل مهمةمرة واحدة عند إنشاء التجمع
أقصى عدد للخيوطغير محدود (خطر OOM)محدد بحجم core/max pool
الاستفادةمنخفضة (ينتهي الخيط بعد المهمة)عالية (يُعاد استخدام الخيط)
الإدارةيدوية (join, interrupt)تلقائية (ExecutorService)
استهلاك الذاكرةينمو مع كل مهمةثابت

كيف يعمل Thread Pool في تطوير التطبيقات المحمولة؟

يعمل تجمع الخيوط وفق مبدأ المنتج-المستهلك: توضع المهام (Runnable/Callable) في قائمة انتظار محجوبة (BlockingQueue). تنتظر الخيوط من التجمع المهام في قائمة الانتظار وتأخذها للتنفيذ. الخوارزمية: إذا كان عدد الخيوط الحرة أقل من corePoolSize، يتم إنشاء خيط جديد. إذا تم الوصول إلى corePoolSize، توضع المهمة في قائمة الانتظار. إذا كانت قائمة الانتظار ممتلئة وكان عدد الخيوط أقل من maximumPoolSize، يتم إنشاء خيط إضافي. إذا تم تجاوز maximumPoolSize، يتم رفض المهمة عبر RejectedExecutionHandler.

Core Pool Size مقابل Maximum Pool Size

Core pool size — عدد الخيوط التي يتم الاحتفاظ بها في التجمع حتى في حالة الخمول. Maximum pool size — أقصى عدد للخيوط يمكن إنشاؤه عند فيضان قائمة الانتظار. الفرق بينهما هو الخيوط الإضافية (overflow) التي يتم إنشاؤها مؤقتاً وتنتهي بعد فترة الخمول. على الأجهزة المحمولة، يُوصى بتعيين corePoolSize مساوياً maximumPoolSize لتجنب الأحمال القصوى الناتجة عن إنشاء الخيوط.

قائمة انتظار العمل و RejectedExecutionHandler

BlockingQueue تخزن المهام التي تنتظر التنفيذ. أكثر التطبيقات شيوعاً: LinkedBlockingQueue (غير محدودة)، ArrayBlockingQueue (محدودة) و SynchronousQueue (بدون تخزين — تُمرر المهمة مباشرة إلى خيط). عند امتلاء قائمة الانتظار والتجمع، يتم تشغيل RejectedExecutionHandler. السياسات القياسية: AbortPolicy (يرمي RejectedExecutionException)، CallerRunsPolicy (ينفذ في خيط المرسل)، DiscardPolicy و DiscardOldestPolicy.

kotlin
// إنشاء Thread Pool في Android
val threadPool = ThreadPoolExecutor(
    corePoolSize = 2,        // الحد الأدنى خيطين
    maximumPoolSize = 4,     // الحد الأقصى 4 خيوط
    keepAliveTime = 30L,     // وقت حياة خيط الفائض
    unit = TimeUnit.SECONDS,
    workQueue = LinkedBlockingQueue<Runnable>(16),
    threadFactory = Executors.defaultThreadFactory(),
    handler = ThreadPoolExecutor.CallerRunsPolicy()
)

// إرسال المهام
threadPool.execute {
    val result = api.fetchData()
    runOnUiThread { showData(result) }
}

// إنهاء التجمع
threadPool.shutdown()
// انتظار اكتمال جميع المهام
threadPool.awaitTermination(10, TimeUnit.SECONDS)

Thread Pool في Android: ExecutorService

يوفر Android عدة تطبيقات لتجمع الخيوط من خلال java.util.concurrent. Executors — مصنع بتكوينات جاهزة: newFixedThreadPool(n) (تجمع ثابت)، newCachedThreadPool() (غير محدود، يتم إنشاء الخيوط حسب الحاجة)، newSingleThreadExecutor() (خيط واحد — تنفيذ تسلسلي). للمشاريع المحمولة، يُوصى باستخدام newFixedThreadPool مع حد معقول (2-4 خيوط)، لأن التجمع المخبأ يمكن أن ينشئ عدداً كبيراً جداً من الخيوط.

ThreadPoolExecutor في Android

ThreadPoolExecutor (TPE) — تطبيق كامل لـ ExecutorService بمعلمات قابلة للتكوين. على Android، يُستخدم TPE داخلياً في AsyncTask و IntentService و JobIntentService. تسمح المعلمات corePoolSize و maximumPoolSize و keepAliveTime و BlockingQueue و RejectedExecutionHandler بضبط سلوك التجمع بدقة. توصيات لـ Android: corePoolSize = عدد أنوية CPU - 1 (للمهام المقيدة بالإدخال/الإخراج) أو عدد الأنوية (للمهام المقيدة بCPU). للتطبيقات النموذجية: 2-4 خيوط.

kotlin
// التكوينات الجاهزة لـ Executors
// 1. تجمع ثابت من 3 خيوط
val fixedPool = Executors.newFixedThreadPool(3)

// 2. تجمع مخبأ (غير موصى به للمحمول)
val cachedPool = Executors.newCachedThreadPool()

// 3. خيط واحد (تسلسلي)
val singlePool = Executors.newSingleThreadExecutor()

// 4. مجدول (مهام دورية)
val scheduler = Executors.newScheduledThreadPool(2)

// الاستخدام مع Callable و Future
val future: Future<String> = fixedPool.submit(Callable {
    "Result: ${api.call()}"
})
// الحصول على النتيجة (يُحجب الخيط)
val result = future.get(5, TimeUnit.SECONDS)

// إنهاء التجمع
fixedPool.shutdownNow()

CoroutineDispatcher كتجمع خيوط

توفر كوروتينات Kotlin CoroutineDispatcher — تجريداً مشابهاً لتجمع الخيوط. يستخدم Dispatchers.IO تجمعاً من 64 خيطاً (محدود). Dispatchers.Default — تجمع مساوٍ لعدد أنوية CPU. لا يتطلب CoroutineDispatcher إيقافاً يدوياً ويُدار تلقائياً. للضبط الدقيق، أنشئ ExecutorCoroutineDispatcher مخصصاً عبر Executors.newFixedThreadPool(2).asCoroutineDispatcher(). لا تستبدل الكوروتينات تجمع الخيوط، بل تغلفه.

Thread Pool في iOS: OperationQueue و GCD

يوفر iOS آليتين رئيسيتين لإدارة تجمع الخيوط: OperationQueue (واجهة برمجة عالية المستوى مبنية على GCD) و GCD DispatchQueue (واجهة برمجة منخفضة المستوى بلغة C). تغلف OperationQueue تجمع الخيوط من خلال الخاصية maxConcurrentOperationCount. افتراضياً، تستخدم OperationQueue حداً أقصى يحدده النظام (يعتمد على حمل النظام). يوفر DispatchQueue.global() قائمة انتظار متزامنة مع تجمع خيوط النظام.

OperationQueue و maxConcurrentOperationCount

OperationQueue تدير تجمع الخيوط من خلال maxConcurrentOperationCount. القيمة 1 تنشئ قائمة انتظار تسلسلية (مشابهة لتجمع خيط واحد). القيمة الأكبر من 1 تنشئ تجمعاً متزامناً بالحد المحدد. افتراضياً، maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (القيمة المثلى للنظام، عادة 4-8 خيوط). تدعم Operation التبعيات والأولويات والإلغاء. يتم تنفيذ كل عملية على أي خيط متاح من تجمع النظام.

swift
// OperationQueue بتجمع من 3 خيوط
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility

// إنشاء العمليات
let operation1 = BlockOperation {
    let data = fetchData(from: url1)
    DispatchQueue.main.async { updateUI(data) }
}

let operation2 = BlockOperation {
    let data = fetchData(from: url2)
    DispatchQueue.main.async { updateUI(data) }
}

// التبعية: operation2 ينتظر operation1
operation2.addDependency(operation1)

// الإضافة إلى قائمة الانتظار
queue.addOperations([operation1, operation2], waitUntilFinished: false)

// إلغاء جميع العمليات
queue.cancelAllOperations()

GCD DispatchQueue كتجمع خيوط

DispatchQueue — هو تجمع خيوط من Apple. تستخدم قائمة الانتظار المتزامنة (qos: .utility) تجمع خيوط النظام، المحسّن لحمل الجهاز الحالي. تتوافق مستويات QoS المختلفة (userInteractive, userInitiated, utility, background) مع تجمعات مختلفة بأولويات مختلفة. DispatchGroup يسمح بمزامنة مهام متعددة. يدعم DispatchWorkItem الإلغاء و qualityOfService. للتحكم الدقيق، أنشئ قوائم انتظار متزامنة مخصصة عبر DispatchQueue(label: qos: attributes: .concurrent).

swift
// GCD DispatchQueue كتجمع خيوط
let customQueue = DispatchQueue(
    label: "com.app.background",
    qos: .utility,
    attributes: .concurrent,
    autoreleaseFrequency: .workItem
)

// إرسال المهام إلى التجمع
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }

// DispatchGroup للمزامنة
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)

pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }

group.notify(queue: .main) {
    self.showResult() // اكتملت كلتا المهمتين
}

// تحديد التزامن عبر semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
    pool.async {
        semaphore.wait()
        download(url)
        semaphore.signal()
    }
}

معلمات تكوين Thread Pool

يؤثر تكوين تجمع الخيوط مباشرة على أداء التطبيق. المعلمات غير الصحيحة تؤدي إلى ضعف استغلال CPU (قلة الخيوط) أو تحميل زائد على النظام (كثرة الخيوط). بالنسبة للتطبيقات المحمولة، تختلف القيم المثلى عن تلك الخاصة بالخادم بسبب الموارد المحدودة واستهلاك الطاقة. المعلمات الرئيسية هي: corePoolSize و maxPoolSize و سعة قائمة الانتظار و keepAliveTime.

حساب الحجم الأمثل للتجمع

صيغة المهام المقيدة بالإدخال/الإخراج: corePoolSize = عدد أنوية CPU × 2 (الخيوط تنتظر الإدخال/الإخراج). للمهام المقيدة بCPU: corePoolSize = عدد أنوية CPU (الخيوط مشغولة باستمرار بالحسابات). على الأجهزة المحمولة الحديثة (6-8 أنوية) يعطي هذا 6-8 خيوط للمهام المقيدة بCPU و 12-16 للمهام المقيدة بالإدخال/الإخراج. تظهر الاختبارات العملية أن 3-4 خيوط مثالية لتطبيق محمول نموذجي — المزيد من الخيوط يزيد استهلاك الطاقة دون تحسين الأداء.

سعة قائمة الانتظار وسلوك الفيضان

يحدد حجم قائمة انتظار العمل (work queue) عدد المهام التي يمكنها انتظار التنفيذ. قائمة انتظار غير محدودة (LinkedBlockingQueue بدون حد) يمكن أن تؤدي إلى OOM عند وصول سريع للمهام. قائمة انتظار محدودة (ArrayBlockingQueue بحجم ثابت) ترفض المهام عند الامتلاء. للتطبيقات المحمولة، يُوصى باستخدام ArrayBlockingQueue بسعة 16-32 مهمة. CallerRunsPolicy هو أفضل RejectedExecutionHandler للمحمول: يبطئ المرسل (الضغط العكسي) بدلاً من فقدان المهمة.

المعلمةتوصية للمحمولالمبرر
corePoolSize2-4الموارد المحدودة للجهاز المحمول
maxPoolSizecorePoolSize (أو +1-2)تجنب الأحمال القصوى من إنشاء الخيوط
keepAliveTime15-30 ثانيةتحرير سريع للذاكرة دون إنشاء متكرر
سعة قائمة الانتظار16-32توازن بين التخزين المؤقت وخطر OOM
المعالجCallerRunsPolicyضغط عكسي دون فقدان المهام

الأخطاء الشائعة عند العمل مع تجمع الخيوط

غالباً ما يرتكب مطورو التطبيقات المحمولة أخطاء عند استخدام تجمع الخيوط تؤدي إلى الأعطال وتسرب الذاكرة والتشغيل غير المستقر. الأكثر شيوعاً: عدم استدعاء shutdown() لـ ExecutorService، إنشاء تجمع جديد لكل عملية، تجمع كبير جداً، deadlock بين المهام، استخدام CachedThreadPool على Android.

Deadlock في Thread Pool

يحدث deadlock عندما تنتظر مهمة في التجمع نتيجة مهمة أخرى من نفس التجمع، لكن جميع الخيوط مشغولة بالانتظار. مثال: ترسل المهمة A المهمة B إلى نفس التجمع وتستدعي future.get() — إذا كان التجمع مستنفذاً، تنتظر المهمة A المهمة B، ولا يمكن للمهمة B التنفيذ لعدم وجود خيوط حرة. الحل: استخدم تجمعات منفصلة لمستويات مختلفة من المهام أو استدعاءات غير متزامنة بدلاً من .get() المحجوب.

kotlin
// Deadlock في Thread Pool
val pool = Executors.newFixedThreadPool(1)

// المهمة أ تنتظر المهمة ب — deadlock!
val futureA = pool.submit {
    // هذه المهمة لن تُنفذ أبداً
    val futureB = pool.submit { 42 }
    futureB.get() // يُحجب إلى الأبد
}

// الحل: تجمعات منفصلة
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()

workerPool.submit {
    callbackPool.submit {
        // يُتنفذ في تجمع منفصل — deadlock مستحيل
    }
}

// أو استخدم CompletableFuture
workerPool.submit {
    CompletableFuture
        .supplyAsync { 42 }
        .thenAccept { result ->
            println(result)
        }
}

التجمع غير المُنهي وتسرب الذاكرة

يجب إنهاء ExecutorService الذي تم إنشاؤه في Activity في onDestroy(). إذا لم يتم ذلك، ستظل الخيوط معلقة في الذاكرة حتى بعد تدمير Activity. الحل: احتفظ بالتجمع في نطاق Application أو ViewModel، وليس في Activity. للكوروتينات، استخدم viewModelScope أو lifecycleScope. إذا تم إنشاء التجمع داخل Activity، تأكد من استدعاء pool.shutdown() في onDestroy(). للاختبار، استخدم es.shutdownNow() للإيقاف الفوري.

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

كيف يختلف Thread Pool عن الخيط العادي؟

Thread Pool يعيد استخدام الخيوط المنشأة بالفعل لتنفيذ مهام متعددة. يتم إنشاء الخيط العادي (raw Thread)، وينفذ مهمة واحدة ثم يُدمر. يستغرق إنشاء الخيط حوالي 1 ميغابايت من الذاكرة و ~1 مللي ثانية من الوقت. يقلل Thread Pool التكاليف الإضافية، ويحد من أقصى عدد للخيوط ويوفر واجهة برمجة للإدارة (shutdown, awaitTermination).

كم عدد الخيوط التي يجب أن تكون في تجمع لتطبيق محمول؟

لتطبيق محمول نموذجي، 2-4 خيوط هي الأمثل. للمهام المقيدة بCPU — عدد أنوية CPU. للمهام المقيدة بالإدخال/الإخراج — عدد الأنوية × 2. زيادة عدد الخيوط تزيد استهلاك الطاقة وتبديل السياق دون تحسين الأداء. على Android، استخدم Process.availableProcessors() لتحديد الأنوية. على iOS — ProcessInfo.processInfo.processorCount.

ما هو CachedThreadPool ولماذا هو خطير على Android؟

CachedThreadPool ينشئ خيوطاً حسب الحاجة ويعيد استخدام الخيوط الموجودة. المشكلة: لا يحد من أقصى عدد للخيوط. إذا وصلت 100 مهمة في وقت واحد، سيتم إنشاء 100 خيط. يؤدي هذا إلى OOM على Android (كل خيط ~1 ميغابايت). استخدم newFixedThreadPool(n) بحد صريح. CachedThreadPool مقبول فقط للمهام السريعة قصيرة المدى بحجم صغير مضمون.

هل أحتاج إلى استدعاء shutdown() لـ ExecutorService؟

نعم، إذا كان التجمع لا ينتمي إلى حاوية مُدارة (مثل الكوروتينات). يوقف shutdown() استقبال المهام الجديدة وينهي الخيوط بعد إكمال المهام الحالية. بدون shutdown()، تظل الخيوط معلقة في الذاكرة ولا ينتهي التطبيق. للنشاط، استدعه في onDestroy(). لـ ViewModel، استخدم coroutineScope. إنهاء التجمع هو جزء إلزامي من إدارة الموارد، مشابه لإغلاق Cursor أو InputStream.

هل OperationQueue و DispatchQueue هما تجمع خيوط؟

نعم، OperationQueue و DispatchQueue هما تجمعا خيوط يوفرهما iOS. يحد OperationQueue التزامن من خلال maxConcurrentOperationCount. يستخدم DispatchQueue.global() تجمع خيوط النظام دون تحكم مباشر. على عكس Java ThreadPoolExecutor، أنت لا تدير corePoolSize أو سعة قائمة الانتظار — يحسّن النظام التجمع تلقائياً بناءً على الحمل الحالي واستهلاك الطاقة للجهاز.

الخلاصة

  • Thread Pool — تجمع خيوط قابلة لإعادة الاستخدام للمهام الخلفية، يقلل التكاليف الإضافية لإنشاء الخيوط.
  • Android يستخدم ThreadPoolExecutor و Executors.newFixedThreadPool(n) مع تحديد صريح لحجم التجمع.
  • iOS يوفر OperationQueue مع maxConcurrentOperationCount و GCD DispatchQueue مع تجمعات QoS.
  • Core pool size — الحد الأدنى لعدد الخيوط؛ maximum pool size — الحد الأقصى عند فيضان قائمة الانتظار.
  • Deadlock في التجمع يحدث عندما تنتظر مهمة مهمة أخرى من نفس التجمع.
  • CallerRunsPolicy مفضل للتطبيقات المحمولة — يبطئ المرسل دون فقدان المهام.
  • لتطبيق محمول نموذجي، الحجم الأمثل للتجمع هو 2-4 خيوط مع shutdown() للتنظيف.

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

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

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

اقرأ أيضًا