Thread Pool در توسعه موبایل — مبانی، استخر نخ‌ها و اصل کار

نویسنده: IT Sectr منتشر شده: 2026-03-18 زمان مطالعه: 11 دقیقه

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 (استخر نخ‌ها) — یک الگوی معماری است که در آن تعداد ثابتی نخ از پیش ایجاد شده و برای اجرای وظایف متعدد مجدداً استفاده می‌شود. به جای ایجاد یک نخ جدید برای هر عملیات (که هزینه‌بر است: حدود ۱ مگابایت پشته برای هر نخ در JVM)، وظایف در یک صف قرار می‌گیرند و توسط نخ‌های آزاد از استخر اجرا می‌شوند. در توسعه موبایل، استخر نخ‌ها برای عملکرد حیاتی است — Android و iOS تعداد نخ‌ها را در هر برنامه محدود می‌کنند.

چرا Thread Pool در توسعه موبایل مهم است؟

ایجاد نخ یک عملیات پرهزینه است: تخصیص پشته، ثبت در سیستم، تغییر زمینه. در دستگاه‌های موبایل با منابع محدود، ایجاد کنترل‌نشده نخ‌ها منجر به OOM (OutOfMemoryError) در Android و محدودسازی (throttling) در iOS می‌شود. Thread Pool هر دو مشکل را حل می‌کند: حداکثر تعداد نخ‌های هم‌زمان را محدود می‌کند و نخ‌های ایجاد شده را مجدداً استفاده می‌کند. Google ExecutorService را به جای raw Thread() توصیه می‌کند، Apple OperationQueue را به جای Thread توصیه می‌کند.

پارامتربدون استخر (raw Thread)با Thread Pool
ایجاد نخبرای هر وظیفهیک بار هنگام ایجاد استخر
حداکثر نخ‌هانامحدود (ریسک OOM)محدود شده توسط core/max pool size
استفادهپایین (نخ پس از وظیفه از بین می‌رود)بالا (نخ مجدداً استفاده می‌شود)
مدیریتدستی (join, interrupt)خودکار (ExecutorService)
مصرف حافظهبا هر وظیفه افزایش می‌یابدثابت

Thread Pool در توسعه موبایل چگونه کار می‌کند؟

استخر نخ‌ها بر اساس اصل Producer-Consumer کار می‌کند: وظایف (Runnable/Callable) در یک صف مسدودکننده (BlockingQueue) قرار می‌گیرند. نخ‌های موجود در استخر منتظر وظایف در صف می‌مانند و آنها را برای اجرا برمی‌دارند. الگوریتم: اگر نخ‌های آزاد کمتر از corePoolSize باشد، یک نخ جدید ایجاد می‌شود. اگر corePoolSize رسیده باشد، وظیفه در صف قرار می‌گیرد. اگر صف پر باشد و نخ‌ها کمتر از maximumPoolSize باشد، یک نخ اضافی ایجاد می‌شود. در صورت تجاوز از maximumPoolSize، وظیفه از طریق RejectedExecutionHandler رد می‌شود.

Core Pool Size در مقابل Maximum Pool Size

Core pool size — تعداد نخ‌هایی که حتی در حالت بیکار در استخر نگه داشته می‌شوند. Maximum pool size — حداکثر تعداد نخ‌هایی که می‌توانند هنگام پر شدن صف ایجاد شوند. تفاوت بین آنها نخ‌های اضافی (overflow) هستند که به طور موقت ایجاد می‌شوند و پس از زمان بیکاری پایان می‌یابند. در دستگاه‌های موبایل توصیه می‌شود corePoolSize برابر maximumPoolSize تنظیم شود تا از بارهای اوج ایجاد نخ جلوگیری شود.

Work Queue و RejectedExecutionHandler

BlockingQueue وظایف منتظر اجرا را ذخیره می‌کند. محبوب‌ترین پیاده‌سازی‌ها: LinkedBlockingQueue (نامحدود)، ArrayBlockingQueue (محدود) و SynchronousQueue (بدون ذخیره‌سازی — وظیفه بلافاصله به نخ منتقل می‌شود). هنگام پر شدن صف و استخر، RejectedExecutionHandler فعال می‌شود. سیاست‌های استاندارد: AbortPolicy (پرتاب RejectedExecutionException)، CallerRunsPolicy (اجرا در نخ فرستنده)، DiscardPolicy و DiscardOldestPolicy.

kotlin
// ایجاد Thread Pool در Android
val threadPool = ThreadPoolExecutor(
    corePoolSize = 2,        // حداقل ۲ نخ
    maximumPoolSize = 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 با محدودیت معقول (۲-۴ نخ) توصیه می‌شود، زیرا cached pool می‌تواند نخ‌های بیش از حد ایجاد کند.

ThreadPoolExecutor در Android

ThreadPoolExecutor (TPE) — پیاده‌سازی کامل ExecutorService با پارامترهای قابل تنظیم. در Android، TPE در داخل AsyncTask، IntentService و JobIntentService استفاده می‌شود. پارامترهای corePoolSize، maximumPoolSize، keepAliveTime، BlockingQueue و RejectedExecutionHandler امکان تنظیم دقیق رفتار استخر را فراهم می‌کنند. توصیه‌ها برای Android: corePoolSize = تعداد هسته‌های CPU - ۱ (برای وظایف IO-bound) یا تعداد هسته‌ها (برای وظایف CPU-bound). برای برنامه‌های معمولی — ۲-۴ نخ.

kotlin
// پیکربندی‌های آماده Executors
// ۱. استخر ثابت برای ۳ نخ
val fixedPool = Executors.newFixedThreadPool(3)

// ۲. استخر کش‌شده (برای موبایل توصیه نمی‌شود)
val cachedPool = Executors.newCachedThreadPool()

// ۳. نخ تکی (سریال‌سازی)
val singlePool = Executors.newSingleThreadExecutor()

// ۴. زمان‌بند (وظایف دوره‌ای)
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 به عنوان استخر نخ

کوروتین‌های کاتلین CoroutineDispatcher را ارائه می‌دهند — انتزاعی مشابه استخر نخ‌ها. Dispatchers.IO از استخر ۶۴ نخ (محدود) استفاده می‌کند. Dispatchers.Default — استخری برابر با تعداد هسته‌های CPU. CoroutineDispatcher به shutdown دستی نیاز ندارد و به طور خودکار مدیریت می‌شود. برای تنظیم دقیق، CoroutineDispatcher سفارشی خود را از طریق Executors.newFixedThreadPool(2).asCoroutineDispatcher() ایجاد کنید. کوروتین‌ها جایگزین استخر نخ‌ها نمی‌شوند، بلکه آن را می‌پوشانند.

Thread Pool در iOS: OperationQueue و GCD

iOS دو مکانیزم اصلی برای مدیریت استخر نخ‌ها ارائه می‌دهد: OperationQueue (API سطح بالا مبتنی بر GCD) و GCD DispatchQueue (API سطح پایین C). OperationQueue استخر نخ‌ها را از طریق ویژگی maxConcurrentOperationCount کپسوله می‌کند. به طور پیش‌فرض، OperationQueue از system-defined maximum (وابسته به بار سیستم) استفاده می‌کند. DispatchQueue.global() یک صف همزمان با استخر نخ سیستمی ارائه می‌دهد.

OperationQueue و maxConcurrentOperationCount

OperationQueue استخر نخ‌ها را از طریق maxConcurrentOperationCount مدیریت می‌کند. مقدار ۱ یک صف ترتیبی ایجاد می‌کند (مشابه استخر تک‌نخی). مقدار بیشتر از ۱ — استخر همزمان با محدودیت مشخص. به طور پیش‌فرض maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (بهینه سیستم، معمولاً ۴-۸ نخ). Operation از وابستگی‌ها، اولویت‌ها و لغو پشتیبانی می‌کند. هر عملیات روی هر نخ آزاد از استخر سیستم اجرا می‌شود.

swift
// OperationQueue با استخر ۳ نخ
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 — استخر نخ اپل است. صف همزمان (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، queue capacity و keepAliveTime.

محاسبه اندازه بهینه استخر

فرمول برای وظایف IO-bound: corePoolSize = تعداد هسته‌های CPU × ۲ (نخ‌ها منتظر ورودی-خروجی هستند). برای وظایف CPU-bound: corePoolSize = تعداد هسته‌های CPU (نخ‌ها دائماً مشغول محاسبات هستند). در دستگاه‌های موبایل مدرن (۶-۸ هسته) این مقادیر ۶-۸ نخ برای CPU-bound و ۱۲-۱۶ برای IO-bound را به دست می‌دهد. تست‌های عملی نشان می‌دهد که برای یک برنامه موبایل معمولی ۳-۴ نخ بهینه است — نخ‌های بیشتر مصرف انرژی را افزایش می‌دهند بدون افزایش عملکرد.

Queue Capacity و رفتار هنگام سرریز

اندازه صف وظایف (work queue) تعیین می‌کند که چند وظیفه می‌توانند منتظر اجرا بمانند. صف نامحدود (LinkedBlockingQueue بدون محدودیت) می‌تواند در صورت ورود سریع وظایف منجر به OOM شود. صف محدود (ArrayBlockingQueue با اندازه ثابت) وظایف را هنگام سرریز رد می‌کند. برای برنامه‌های موبایل، ArrayBlockingQueue با ظرفیت ۱۶-۳۲ وظیفه توصیه می‌شود. CallerRunsPolicy — بهترین RejectedExecutionHandler برای دستگاه‌های موبایل: به جای از دست دادن وظیفه، فرستنده را کند می‌کند (pressure back).

پارامترتوصیه برای موبایلدلیل
corePoolSize۲-۴منابع محدود دستگاه موبایل
maxPoolSizecorePoolSize (یا +۱-۲)جلوگیری از بارهای اوج ایجاد نخ
keepAliveTime۱۵-۳۰ ثانیهآزادسازی سریع حافظه، اما بدون ایجاد مکرر
Queue capacity۱۶-۳۲تعادل بین بافرینگ و ریسک OOM
HandlerCallerRunsPolicyفشار برگشتی بدون از دست دادن وظایف

اشتباهات رایج هنگام کار با استخر نخ‌ها

توسعه‌دهندگان برنامه‌های موبایل اغلب هنگام استفاده از استخر نخ‌ها اشتباهاتی مرتکب می‌شوند که منجر به کرش، نشت حافظه و عملکرد ناپایدار می‌شود. رایج‌ترین: فراخوانی نکردن shutdown() برای ExecutorService، ایجاد استخر جدید برای هر عملیات، استخر بسیار بزرگ، deadlock بین وظایف، استفاده از CachedThreadPool در Android.

Deadlock در Thread Pool

Deadlock زمانی رخ می‌دهد که یک وظیفه در استخر منتظر نتیجه وظیفه دیگری از همان استخر است، اما همه نخ‌ها مشغول انتظار هستند. مثال: وظیفه A وظیفه B را به همان استخر می‌فرستد و future.get() را فراخوانی می‌کند — اگر استخر تمام شده باشد، وظیفه A منتظر وظیفه B می‌ماند و وظیفه B نمی‌تواند اجرا شود زیرا نخ آزادی وجود ندارد. راه‌حل: از استخرهای جداگانه برای سطوح مختلف وظایف یا callback ناهمگام به جای .get() مسدودکننده استفاده کنید.

kotlin
// Deadlock در Thread Pool
val pool = Executors.newFixedThreadPool(1)

// وظیفه A منتظر وظیفه B است — 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) ایجاد می‌شود، یک وظیفه را اجرا می‌کند و نابود می‌شود. ایجاد نخ حدود ۱ مگابایت حافظه و ~۱ میلی‌ثانیه زمان می‌برد. Thread Pool هزینه‌های سربار را کاهش می‌دهد، حداکثر تعداد نخ‌ها را محدود می‌کند و API مدیریت (shutdown, awaitTermination) ارائه می‌دهد.

چه تعداد نخ باید در استخر برای یک برنامه موبایل باشد؟

برای یک برنامه موبایل معمولی ۲-۴ نخ بهینه است. برای وظایف CPU-bound — تعداد هسته‌های CPU. برای وظایف IO-bound — تعداد هسته‌ها × ۲. تعداد بیشتر نخ‌ها مصرف انرژی و تغییر زمینه را بدون افزایش عملکرد افزایش می‌دهد. در Android از Process.availableProcessors() برای تعیین هسته‌ها استفاده کنید. در iOS — ProcessInfo.processInfo.processorCount.

CachedThreadPool چیست و چرا در Android خطرناک است؟

CachedThreadPool در صورت نیاز نخ ایجاد می‌کند و نخ‌های موجود را مجدداً استفاده می‌کند. مشکل: حداکثر تعداد نخ‌ها را محدود نمی‌کند. اگر ۱۰۰ وظیفه همزمان برسند، ۱۰۰ نخ ایجاد می‌شود. این منجر به OOM در Android می‌شود (هر نخ ~۱ مگابایت). از newFixedThreadPool(n) با محدودیت صریح استفاده کنید. CachedThreadPool فقط برای وظایف burst کوتاه‌مدت با تضمین حجم کم مجاز است.

آیا باید shutdown() را برای ExecutorService فراخوانی کرد؟

بله، اگر استخر به یک کانتینر مدیریت‌شده (مانند کوروتین‌ها) تعلق نداشته باشد. shutdown() پذیرش وظایف جدید را متوقف می‌کند و نخ‌ها را پس از اتمام وظایف جاری پایان می‌دهد. بدون shutdown() نخ‌ها در حافظه باقی می‌مانند و برنامه پایان نمی‌یابد. برای Activity در onDestroy() فراخوانی کنید. برای ViewModel از coroutineScope استفاده کنید. پایان دادن به استخر بخش الزامی مدیریت منابع است، مشابه بستن Cursor یا InputStream.

آیا OperationQueue و DispatchQueue استخر نخ هستند؟

بله، OperationQueue و DispatchQueue استخر نخ ارائه‌شده توسط iOS هستند. OperationQueue هم‌زمانی را از طریق maxConcurrentOperationCount محدود می‌کند. DispatchQueue.global() از استخر نخ سیستمی بدون کنترل مستقیم استفاده می‌کند. برخلاف Java ThreadPoolExecutor، شما corePoolSize یا queue capacity را مدیریت نمی‌کنید — سیستم استخر را به طور خودکار تحت بار فعلی و مصرف انرژی دستگاه بهینه می‌کند.

خلاصه

  • Thread Pool — استخری از نخ‌های قابل استفاده مجدد برای اجرای وظایف پس‌زمینه که هزینه‌های ایجاد نخ را کاهش می‌دهد.
  • Android از ThreadPoolExecutor و Executors.newFixedThreadPool(n) با محدودیت صریح اندازه استخر استفاده می‌کند.
  • iOS OperationQueue با maxConcurrentOperationCount و GCD DispatchQueue با استخرهای QoS ارائه می‌دهد.
  • Core pool size — حداقل تعداد نخ‌ها؛ maximum pool size — حداکثر هنگام پر شدن صف.
  • Deadlock در استخر زمانی رخ می‌دهد که یک وظیفه منتظر وظیفه دیگری از همان استخر باشد.
  • CallerRunsPolicy برای برنامه‌های موبایل ترجیح داده می‌شود — بدون از دست دادن وظایف، فرستنده را کند می‌کند.
  • اندازه بهینه استخر برای یک برنامه موبایل معمولی ۲-۴ نخ با بستن از طریق shutdown() است.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید