Background Thread — نخ اجرایی غیرمرتبط با رابط کاربری، طراحی شده برای عملیات طولانی: درخواستهای شبکه، کار با فایلها، تجزیه JSON، فشردهسازی تصاویر، رمزگذاری و درخواستهای پایگاه داده. در iOS نخهای پسزمینه از طریق GCD (DispatchQueue.global) و OperationQueue مدیریت میشوند، در Android — از طریق Executors، WorkManager و Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default). به گفته Apple DispatchQueue Documentation, پس از اتمام عملیات پسزمینه نتیجه باید برای بهروزرسانی رابط به Main Thread بازگردانده شود.
نکات اصلی
Background Thread — هر نخی در برنامه که Main Thread نیست و به UI دسترسی ندارد. وظیفه آن آزاد کردن نخ اصلی از عملیات سنگین است تا رابط کاربری پاسخگو باقی بماند. سیستم عامل نخهای پسزمینه را بین هستههای پردازنده توزیع میکند و امکان اجرای همزمان چندین وظیفه را فراهم میکند. iOS به طور خودکار pool نخها را از طریق GCD مدیریت میکند، Android — از طریق poolهای Java Executors.
برخلاف Main Thread که رویدادها را به صورت ترتیبی (یکی پس از دیگری) پردازش میکند، نخهای پسزمینه میتوانند به صورت موازی اجرا شوند و تنها با تعداد هستههای CPU محدود میشوند. به عنوان مثال، در دستگاهی با 8 هسته میتوان تا 8 وظیفه پسزمینه موازی بدون کاهش سرعت قابل توجه اجرا کرد. با این حال، تعداد بیش از حد نخها (صدها) منجر به thread starvation میشود — رقابت برای هستهها و افزایش هزینههای سربار تغییر زمینه (context switch).
Quality of Service (QoS) — مکانیزم iOS که به شما امکان میدهد اولویت وظیفه پسزمینه را مشخص کنید. مقادیر: .userInteractive (بالاترین، تقریباً Main Thread), .userInitiated (کاربر منتظر نتیجه است), .default (استاندارد), .utility (کاربر مستقیماً منتظر نیست), .background (پایینترین، برای همگامسازی و نمایهسازی). در Android معادل Thread.setPriority() از 1 تا 10 است، اما Android همچنین از cgroups برای مدیریت گروهی اولویتهای نخ استفاده میکند.
DispatchQueue.global(qos:) — روش اصلی دریافت صف پسزمینه در iOS. GCD (Grand Central Dispatch) به طور خودکار pool نخ ایجاد میکند و وظایف را بین هستهها توزیع میکند. فراخوانی DispatchQueue.global(qos: .background).async {} بلوک اجرا را به صف پسزمینه با کمترین اولویت میفرستد. برای وظایفی که نتیجه آنها فوراً مورد نیاز است، از .userInitiated یا .utility استفاده کنید.
OperationQueue — انتزاع سطح بالاتر بر روی GCD که امکان تعیین وابستگیهای بین عملیات، حداکثر تعداد عملیات همزمان (maxConcurrentOperationCount) و اولویتها را فراهم میکند. OperationQueue برای زنجیرههای پیچیده چندوظیفهای مناسب است: دانلود فایل -> باز کردن -> ذخیره در حافظه نهان. به طور پیشفرض، OperationQueue از نخهای پسزمینه استفاده میکند مگر اینکه طور دیگری مشخص شود.
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// OperationQueue با maxConcurrentOperationCount = 2
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .utility
for urlString in urls {
queue.addOperation {
guard let url = URL(string: urlString),
let data = try? Data(contentsOf: url)
else { return }
DispatchQueue.main.async {
print("دانلود شد: \(url.lastPathComponent)")
}
}
}
}
// GCD: صف جهانی پسزمینه با QoS مختلف
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// اولویت بالا — کاربر منتظر نتیجه است
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // شبیهسازی کار
return "نتیجه محاسبات"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
در مثال OperationQueue دو تصویر را به صورت موازی بارگیری میکند (maxConcurrentOperationCount = 2) با پسزمینه از طریق qualityOfService = .utility. روش GCD backgroundTaskWithQoS از صف جهانی با .userInitiated برای وظیفهای که کاربر منتظر نتیجه آن است استفاده میکند. هر دو رویکرد با بازگشت به DispatchQueue.main برای بهروزرسانی UI پایان مییابند — این یک الزام اجباری iOS است.
GCD از دو نوع صف پشتیبانی میکند: serial (ترتیبی) و concurrent (موازی). صفهای serial وظایف را یکی پس از دیگری اجرا میکنند — این برای دسترسی به یک منبع مشترک (فایل، پایگاه داده) بدون قفل مناسب است. صفهای concurrent وظایف را به صورت موازی اجرا میکنند و آنها را بین هستههای آزاد توزیع میکنند. DispatchQueue.global — همیشه concurrent. برای ایجاد صف serial از DispatchQueue(label: "com.app.queue") استفاده کنید.
Android چندین سطح انتزاع برای نخهای پسزمینه ارائه میدهد. رویکرد کلاسیک — java.util.concurrent.Executors.newFixedThreadPool(n) یا Executors.newCachedThreadPool(). رویکرد مدرن — Kotlin Coroutines با Dispatchers.IO (برای ورودی/خروجی: شبکه، فایلها، پایگاه داده) و Dispatchers.Default (برای وظایف سنگین CPU: مرتبسازی، پردازش تصاویر). WorkManager — برای وظایف پسزمینه تأخیری و تضمینی.
HandlerThread — کلاس مخصوص Android برای ایجاد نخ پسزمینه با Looper (صف پیام) خود. برخلاف Executors، HandlerThread اجازه ارسال پیامها و Runnable را از طریق Handler میدهد. برای عملیاتی که نیاز به صفبندی دارند (مثلاً نوشتن ترتیبی در پایگاه داده) استفاده میشود. پس از استفاده باید quit() یا quitSafely() برای آزادسازی منابع فراخوانی شود.
// Android: Executors و Coroutines Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// رویکرد کلاسیک از طریق Executors
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// رویکرد مدرن از طریق Coroutines
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// عملیات فایل — در pool پسزمینه اجرا میشود
readFromFile()
}
// نتیجه به طور خودکار به Dispatchers.Main بازمیگردد
}
// وظیفه سنگین CPU روی Dispatchers.Default
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// مرتبسازی، فیلتر کردن — روی Default-pool اجرا میشود
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // شبیهسازی خواندن از فایل
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
مثال DataRepository تکامل نخهای پسزمینه در Android را نشان میدهد. روش legacy loadDataLegacy از Executors.newFixedThreadPool(4) با Handler برای بازگشت به Main Thread استفاده میکند. روش مدرن loadDataCoroutines از withContext(Dispatchers.IO) استفاده میکند — کروتین در طول کار متوقف میشود، نخ را مسدود نمیکند و به طور خودکار در Main Thread از سر گرفته میشود. Dispatchers.Default برای عملیات CPU-bound (مرتبسازی، فیلتر کردن، تبدیل داده) توصیه میشود.
Kotlin Coroutines — نه فقط یک روش کار با نخها، بلکه یک مدل اساساً متفاوت: وظایف ناهمزمان به یک نخ خاص متصل نیستند و میتوانند بدون مسدودسازی معلق شوند (suspend). این بدان معناست که در حین کار پسزمینه، کروتین نخ را اشغال نمیکند، بلکه آن را برای وظایف دیگر آزاد میکند. مکانیزم suspension امکان اجرای صدها هزار وظیفه concurrent را در pool 4-8 نخ بدون thread starvation فراهم میکند.
سه dispatcher اصلی: Dispatchers.Main (UI، یک نخ), Dispatchers.IO (64 نخ به طور پیشفرض برای عملیات مسدودکننده: شبکه، فایلها، پایگاه داده), Dispatchers.Default (برابر با تعداد هستههای CPU، برای محاسبات فشرده). با ترکیب آنها از طریق withContext، توسعهدهنده بین نخها بدون ایجاد callback جابهجا میشود. withContext یک تابع suspend است که تا زمانی که وظیفه انجام نشده، کنترل را بازنمیگرداند.
// کروتینها: ترکیب وظایف پسزمینه
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// بارگیری موازی داده از منابع مختلف
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — تا تکمیل همه وظایف معلق میشود
UserProfile(
user = user.await(),
posts = posts.await(),
avatar = avatar.await()
)
}
data class UserProfile(
val user: String,
val posts: List<String>,
val avatar: ByteArray
)
suspend fun fetchUser(id: String): String { delay(300); return "User:$id" }
suspend fun fetchPosts(id: String): List<String> { delay(500); return listOf("Post1") }
suspend fun fetchAvatar(id: String): ByteArray { delay(200); return ByteArray(1024) }
suspend fun processAvatar(data: ByteArray): ByteArray { delay(100); return data }
تابع loadUserProfile سه وظیفه پسزمینه موازی را از طریق async راهاندازی میکند. fetchUser و fetchPosts — IO-bound (شبکه)، روی Dispatchers.IO اجرا میشوند. processAvatar — CPU-bound (پردازش تصویر)، روی Dispatchers.Default اجرا میشود. await() کروتین را تا تکمیل همه وظایف معلق میکند. زمان کل اجرا برابر با حداکثر زمان بین سه وظیفه است (500 میلیثانیه برای fetchPosts)، نه مجموع آنها. این مزیت کلیدی coroutines نسبت به اجرای ترتیبی است.
Structured concurrency — اصلی که در آن هر کروتین یک scope والد دارد و لغو والد به طور خودکار کروتینهای فرزند را لغو میکند. در Android lifecycleScope تمام کروتینها را هنگام نابودی Activity لغو میکند. viewModelScope — هنگام پاکسازی ViewModel. این از نشت وظایف پسزمینه جلوگیری میکند: اگر کاربر صفحه را بست، کروتین پسزمینه به بارگیری دادههایی که دیگر به درد کسی نمیخورند ادامه نخواهد داد.
WorkManager — کتابخانه Android Jetpack برای اجرای وظایف پسزمینه که حتی پس از راهاندازی مجدد دستگاه یا بسته شدن برنامه باید اجرا شوند. برخلاف Executors و کروتینها که در فرآیند برنامه زندگی میکنند، WorkManager وظیفه را به dispatcher سیستم میسپرد که اجرا را در شرایط مناسب (دسترسی به شبکه، سطح باتری، فضای خالی) تضمین میکند. WorkManager برای همگامسازی دادهها، آپلود لاگها، پشتیبانگیری مناسب است.
وظیفه در WorkManager کلاسی است که از Worker (یا CoroutineWorker برای کروتینها) ارث میبرد. Worker.doWork() روی نخ پسزمینه ارائه شده توسط WorkManager اجرا میشود. نتیجه از طریق Result.success(), Result.retry() یا Result.failure() بازگردانده میشود. وظایف را میتوان در زنجیرهها ترکیب کرد: oneTimeWorkRequest.andThen(nextRequest).enqueue(). WorkManager خود زمان بهینه اجرا را با در نظر گرفتن محدودیتها (Constraints) انتخاب میکند.
// WorkManager با کروتینها
import android.content.Context
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SyncWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// روی Dispatchers.Default اجرا میشود (پیشفرض)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// شبیهسازی همگامسازی
delay(1000)
}
}
// راهاندازی وظیفه WorkManager با محدودیتها
fun scheduleSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, java.util.concurrent.TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(syncWork)
}
SyncWorker از CoroutineWorker — نسخه Worker پشتیبانیکننده از کروتینها ارث میبرد. doWork() روی Dispatchers.Default اجرا میشود، تغییر به IO برای عملیات شبکه از طریق withContext. Constraints تضمین میکند که همگامسازی فقط در صورت وجود شبکه و سطح باتری نه کمتر از پایین شروع شود. BackoffCriteria با EXPONENTIAL فاصله بین تلاشهای مجدد را افزایش میدهد: 10، 20، 40 ثانیه.
برای وظایف منظم (همگامسازی هر 15 دقیقه، ارسال تحلیل هر ساعت) WorkManager PeriodicWorkRequestBuilder را ارائه میدهد. حداقل فاصله — 15 دقیقه. برخلاف OneTimeWorkRequest، PeriodicWorkRequest رعایت دقیق فاصله را تضمین نمیکند — سیستم ممکن است چندین وظیفه دورهای را برای صرفهجویی در باتری ترکیب کند. برای فواصل دقیق از AlarmManager استفاده کنید، اما محدودیتهای Android 12+ را برای آلارمهای دقیق در نظر بگیرید.
خطای اول — ایجاد Thread جدید برای هر وظیفه. new Thread().start() یک نخ بومی با تخصیص ~1 مگابایت برای پشته ایجاد میکند. برای 100 وظیفه موازی این 100 مگابایت فقط برای پشتههاست، به علاوه هزینههای سربار context switch. از poolهای نخ استفاده کنید: Executors.newFixedThreadPool(n) (Android) یا DispatchQueue.global() (iOS) — آنها نخها را مجدداً استفاده میکنند و overhead را دهها برابر کاهش میدهند.
خطای دوم — دسترسی به حالت mutable از چندین نخ پسزمینه بدون همگامسازی. اگر دو نخ پسزمینه همزمان در یک ArrayList یا HashMap بنویسند، race condition رخ میدهد: ConcurrentModificationException در Android، خرابی داده در iOS. راهحل: از مجموعههای thread-safe (ConcurrentHashMap, CopyOnWriteArrayList) استفاده کنید یا دسترسی را از طریق یک صف (DispatchQueue serial) سریالی کنید.
خطای سوم — وظایف پسزمینه بدون مدیریت چرخه حیات. راهاندازی کروتین در scope جهانی بدون اتصال به چرخه حیات Activity یا ViewModel منجر به نشت میشود: وظیفه پس از نابودی صفحه به اجرا ادامه میدهد. در Android از lifecycleScope (Activity/Fragment) یا viewModelScope (ViewModel) استفاده کنید. در iOS — weak self در closureها و لغو وظایف در هنگام deinit.
سوالات متداول
Background Thread — نخی که عملیات غیرمرتبط با UI روی آن اجرا میشود: درخواستهای شبکه، خواندن/نوشتن فایلها، تجزیه JSON، محاسبات. این نخ Main Thread را از کار سنگین آزاد میکند و پاسخگویی رابط را حفظ میکند. در iOS نخهای پسزمینه از طریق GCD (DispatchQueue.global) مدیریت میشوند، در Android — از طریق Executors یا Kotlin Coroutines (Dispatchers.IO, Dispatchers.Default).
Dispatchers.IO برای عملیات مسدودکننده ورودی/خروجی طراحی شده است: خواندن فایلها، درخواستهای شبکه، کار با پایگاه داده. این dispatcher یک pool از 64 نخ دارد. Dispatchers.Default — برای وظایف سنگین CPU: مرتبسازی، فیلتر کردن، پردازش تصاویر. pool آن برابر با تعداد هستههای CPU است. استفاده از Dispatchers.Default برای عملیات IO میتواند همه هستهها را مسدود کند، و Dispatchers.IO برای وظایف CPU — تعداد بیش از حد نخ ایجاد کند.
DispatchQueue.global(qos: .background).async { } بلوک اجرا را به صف جهانی پسزمینه میفرستد. پس از اتمام کار پسزمینه باید برای بهروزرسانی UI از طریق DispatchQueue.main.async { } به نخ اصلی بازگشت. برای وظایف ترتیبی پسزمینه از OperationQueue با maxConcurrentOperationCount = 1 یا DispatchQueue(label: "serial") استفاده کنید.
تعداد توصیهشده نخهای پسزمینه برابر با تعداد هستههای CPU به علاوه 1 برای وظایف IO-bound است. در یک دستگاه مدرن 8 هستهای این 9 نخ است. ایجاد صدها نخ منجر به thread starvation میشود: سیستم عامل زمان بیشتری را صرف تغییر زمینه (context switch) میکند تا اجرای وظایف. GCD در iOS و Executors در Android به طور خودکار pool نخ را برای دستگاه فعلی بهینه میکنند.
در Kotlin Coroutines بازگشت به Main Thread به طور خودکار انجام میشود اگر کروتین در Main-scope راهاندازی شده باشد (lifecycleScope.launch, viewModelScope.launch). تابع withContext(Dispatchers.IO) کروتین را در نخ IO متوقف میکند و پس از اتمام به طور خودکار آن را در dispatcherای که در آن راهاندازی شده (معمولاً Main) از سر میگیرد. فراخوانی صریح DispatchQueue.main.async مورد نیاز نیست.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید