Background Thread في تطوير التطبيقات المحمولة: ما هو، المهام وطرق الاستخدام

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

Background Thread — خيط تنفيذ غير مرتبط بواجهة المستخدم، مخصص للعمليات الطويلة: طلبات الشبكة، العمل مع الملفات، تحليل JSON، ضغط الصور، التشفير واستعلامات قاعدة البيانات. في iOS، تتم إدارة خيوط الخلفية عبر GCD (DispatchQueue.global) و OperationQueue؛ في Android، عبر Executors و WorkManager و Kotlin Coroutines (Dispatchers.IO، Dispatchers.Default). وفقًا لـ توثيق Apple DispatchQueue، بعد اكتمال العملية في الخلفية، يجب إرجاع النتيجة إلى Main Thread لتحديث الواجهة.

الخلاصة

  • Background Thread ينفذ العمليات التي تحظر UI: الشبكة، الملفات، JSON، الحسابات
  • iOS: DispatchQueue.global(qos:) و OperationQueue للمهام الخلفية
  • Android: Dispatchers.IO (الشبكة/الملفات)، Dispatchers.Default (الحسابات)، WorkManager (المهام الخلفية)
  • Coroutines — المعيار الحديث للعمل الخلفي: withContext(Dispatchers.IO) يبدل الخيط دون callback hell
  • النتيجة من Background Thread تُعاد دائمًا إلى Main Thread لتحديث UI

ما هو Background Thread

Background Thread — أي خيط في التطبيق ليس Main Thread وليس لديه حق الوصول إلى UI. مهمته هي تحرير الخيط الرئيسي من العمليات الثقيلة للحفاظ على استجابة الواجهة. يقوم نظام التشغيل بتوزيع خيوط الخلفية على أنوية المعالج، مما يسمح بتنفيذ مهام متعددة بالتوازي. يدير iOS مجموعة الخيوط تلقائيًا عبر GCD، ويديرها Android عبر مجموعات Java Executors.

على عكس Main Thread الذي يعالج الأحداث بالتسلسل (واحدًا تلو الآخر)، يمكن لخيوط الخلفية التنفيذ بالتوازي، محدودة فقط بعدد أنوية المعالج. على سبيل المثال، على جهاز بثمانية أنوية يمكن تشغيل ما يصل إلى 8 مهام خلفية متوازية دون تباطؤ كبير. ومع ذلك، يؤدي العدد المفرط من الخيوط (المئات) إلى thread starvation — التنافس على الأنوية وزيادة الحمل الناتج عن تبديل السياق (context switch).

Quality of Service (QoS) — آلية في iOS تسمح بتحديد أولوية المهمة الخلفية. القيم: .userInteractive (الأعلى، قريب من Main Thread)، .userInitiated (المستخدم ينتظر النتيجة)، .default (قياسي)، .utility (المستخدم لا ينتظر مباشرة)، .background (الأدنى، للمزامنة والفهرسة). في Android، المعادل هو Thread.setPriority() من 1 إلى 10، لكن Android يستخدم أيضًا cgroups للإدارة الجماعية لأولويات الخيوط.

Background Thread في iOS: GCD و DispatchQueue.global

DispatchQueue.global(qos:) — الطريقة الرئيسية للحصول على قائمة انتظار خلفية في iOS. يقوم GCD (Grand Central Dispatch) بإنشاء مجموعة خيوط تلقائيًا وتوزيع المهام على الأنوية. الاستدعاء DispatchQueue.global(qos: .background).async {} يرسل كتلة إلى قائمة الانتظار الخلفية بأقل أولوية. للمهام التي تحتاج نتيجتها فورًا، استخدم .userInitiated أو .utility.

OperationQueue — تجريد عالي المستوى فوق GCD يسمح بتعيين التبعيات بين العمليات، والحد الأقصى لعدد العمليات المتزامنة (maxConcurrentOperationCount)، والأولويات. OperationQueue مفيدة للسلاسل المعقدة متعددة المهام: تنزيل ملف → فك ضغط → حفظ في ذاكرة التخزين المؤقت. افتراضيًا، تستخدم OperationQueue خيوط الخلفية ما لم يُذكر خلاف ذلك.

swift
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) مع QoS خلفية عبر qualityOfService = .utility. تستخدم طريقة GCD backgroundTaskWithQoS قائمة انتظار عامة مع .userInitiated لمهمة ينتظر المستخدم نتيجتها. ينتهي كلا الأسلوبين بالعودة إلى DispatchQueue.main لتحديث UI — وهذا مطلب إلزامي في iOS.

قوائم الانتظار الخلفية Serial vs Concurrent

يدعم GCD نوعين من قوائم الانتظار: serial (تسلسلية) و concurrent (متزامنة). تنفذ قوائم الانتظار التسلسلية المهام واحدة تلو الأخرى — وهذا مناسب للوصول إلى مورد مشترك (ملف، قاعدة بيانات) دون أقفال. تنفذ قوائم الانتظار المتزامنة المهام بالتوازي، موزعة على الأنوية المتاحة. DispatchQueue.global دائمًا متزامنة. لإنشاء قائمة انتظار تسلسلية، استخدم DispatchQueue(label: "com.app.queue").

Background Thread في Android: Executors و Dispatchers

Android يوفر عدة مستويات من التجريد لخيوط الخلفية. النهج الكلاسيكي هو java.util.concurrent.Executors.newFixedThreadPool(n) أو Executors.newCachedThreadPool(). النهج الحديث هو Kotlin Coroutines مع Dispatchers.IO (للإدخال/الإخراج: الشبكة، الملفات، قاعدة البيانات) و Dispatchers.Default (للمهام المكثفة للمعالج: الفرز، معالجة الصور). WorkManager للمهام الخلفية المؤجلة والمضمونة.

HandlerThread — فئة متخصصة في Android لإنشاء خيط خلفي مع Looper خاص به (قائمة انتظار الرسائل). على عكس Executors، يسمح HandlerThread بإرسال الرسائل و Runnables عبر Handler. يُستخدم للعمليات التي تتطلب وضعًا في قائمة الانتظار (مثل الكتابة التسلسلية في قاعدة البيانات). بعد الاستخدام، يجب استدعاء quit() أو quitSafely() لتحرير الموارد.

kotlin
// 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) {
            // عملية ملف — تُنفذ في مجمع خلفي
            readFromFile()
        }
        // النتيجة تعود تلقائيًا إلى Dispatchers.Main
    }

    // مهمة مكثفة للمعالج على Dispatchers.Default
    suspend fun processImage(pixels: IntArray): IntArray {
        return withContext(Dispatchers.Default) {
            // الفرز، التصفية — تُنفذ على مجمع Default
            pixels.sortedArray()
        }
    }

    private fun readFromFile(): String {
        Thread.sleep(1000) // محاكاة قراءة من ملف
        return "file_content"
    }

    fun cleanup() {
        ioExecutor.shutdown()
    }
}

مثال DataRepository يوضح تطور خيوط الخلفية في Android. الطريقة القديمة loadDataLegacy تستخدم Executors.newFixedThreadPool(4) مع Handler للعودة إلى Main Thread. الطريقة الحديثة loadDataCoroutines تستخدم withContext(Dispatchers.IO) — يتم تعليق coroutine أثناء التنفيذ دون حظر الخيط، وتستأنف تلقائيًا على Main Thread. يُوصى باستخدام Dispatchers.Default للعمليات المكثفة للمعالج (الفرز، التصفية، تحويل البيانات).

Coroutines كمعيار حديث للمهام الخلفية

Kotlin Coroutines — ليست مجرد طريقة للعمل مع الخيوط، بل نموذج مختلف جوهريًا: المهام غير المتزامنة غير مرتبطة بخيط معين ويمكن تعليقها دون حظر. هذا يعني أنه في الخلفية، لا يشغل coroutine خيطًا بل يحرره للمهام الأخرى. تسمح آلية التعليق بتنفيذ مئات الآلاف من المهام المتزامنة على مجموعة من 4–8 خيوط دون thread starvation.

ثلاثة موزعين رئيسيين: Dispatchers.Main (UI، خيط واحد)، Dispatchers.IO (64 خيطًا افتراضيًا للعمليات المحظورة: الشبكة، الملفات، قاعدة البيانات)، Dispatchers.Default (يساوي عدد أنوية المعالج، للحسابات المكثفة). من خلال دمجها عبر withContext، يبدل المطور بين الخيوط دون إنشاء callbacks. withContext هي دالة suspend لا تعود بالتحكم حتى اكتمال المهمة.

kotlin
// Coroutines: تركيب المهام الخلفية
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 (شبكة)، تُنفذ على Dispatchers.IO. processAvatar هي مهمة مكثفة للمعالج (معالجة صور)، تُنفذ على Dispatchers.Default. await() تعلق coroutine حتى اكتمال جميع المهام. إجمالي وقت التنفيذ يساوي أقصى وقت بين المهام الثلاث (500 مللي ثانية لـ fetchPosts)، وليس مجموعها. هذه ميزة رئيسية لـ coroutines على التنفيذ التسلسلي.

التزامن المنظم: منع التسريبات

Structured concurrency — مبدأ حيث يكون لكل coroutine نطاق أبوي، وإلغاء الأب يلغي تلقائيًا coroutines الفرعية. في Android، lifecycleScope يلغي جميع coroutines عند تدمير Activity. viewModelScope يفعل ذلك عند تنظيف ViewModel. هذا يمنع تسرب المهام الخلفية: إذا أغلق المستخدم الشاشة، في الخلفية لن يستمر coroutine في تحميل بيانات لم تعد مطلوبة.

WorkManager: المهام الخلفية لنظام Android

WorkManager — مكتبة Android Jetpack لتنفيذ المهام الخلفية التي يجب إكمالها حتى بعد إعادة تشغيل الجهاز أو إغلاق التطبيق. على عكس Executors و coroutines التي تعيش داخل عملية التطبيق، يسلم WorkManager المهمة إلى موزع النظام الذي يضمن التنفيذ تحت الظروف المناسبة (توفر الشبكة، شحن البطارية، مساحة خالية). WorkManager مناسب لمزامنة البيانات، تحميل السجلات، والنسخ الاحتياطي.

المهمة في WorkManager هي فئة ترث Worker (أو CoroutineWorker لـ coroutines). يتم تنفيذ Worker.doWork() على خيط خلفي يوفره WorkManager. يتم إرجاع النتيجة عبر Result.success() أو Result.retry() أو Result.failure(). يمكن ربط المهام في سلاسل: oneTimeWorkRequest.andThen(nextRequest).enqueue(). يختار WorkManager وقت التنفيذ الأمثل مع مراعاة القيود.

kotlin
// WorkManager مع coroutines
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 يدعم coroutines. يتم تنفيذ doWork() على Dispatchers.Default، مع التبديل إلى IO للعمليات الشبكية عبر withContext. تضمن القيود (Constraints) أن تبدأ المزامنة فقط عند توفر الشبكة وشحن البطارية عند مستوى لا يقل عن منخفض. يزيد BackoffCriteria مع EXPONENTIAL الفاصل الزمني بين محاولات إعادة المحاولة: 10، 20، 40 ثانية.

PeriodicWorkRequest للمهام الخلفية الدورية

لللمهام الدورية (المزامنة كل 15 دقيقة، إرسال التحليلات كل ساعة)، يوفر WorkManager PeriodicWorkRequestBuilder. الحد الأدنى للفاصل الزمني هو 15 دقيقة. على عكس OneTimeWorkRequest، لا يضمن PeriodicWorkRequest الالتزام الدقيق بالفاصل الزمني — قد يقوم النظام بتجميع عدة مهام دورية لتوفير البطارية. للفواصل الزمنية الدقيقة، استخدم AlarmManager، لكن ضع في اعتبارك قيود Android 12+ على المنبهات الدقيقة.

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

الخطأ الأول — إنشاء خيط جديد لكل مهمة. new Thread().start() ينشئ خيطًا أصليًا يخصص ~1 MB للمكدس. لـ 100 مهمة متوازية، هذا يعني 100 ميجابايت فقط للمكدسات، بالإضافة إلى الحمل الزائد لتبديل السياق. استخدم مجمعات الخيوط: Executors.newFixedThreadPool(n) (Android) أو DispatchQueue.global() (iOS) — تعيد استخدام الخيوط، مما يقلل الحمل الزائد بعشرات المرات.

الخطأ الثاني — الوصول إلى حالة قابلة للتغيير من خيوط خلفية متعددة دون مزامنة. إذا كتب خيطان خلفيان في نفس ArrayList أو HashMap في وقت واحد، تحدث حالات سباق (race conditions): ConcurrentModificationException في Android، تلف البيانات في iOS. الحل: استخدم مجموعات آمنة للخيوط (ConcurrentHashMap، CopyOnWriteArrayList) أو قم بتسلسل الوصول عبر قائمة انتظار واحدة (DispatchQueue serial).

الخطأ الثالث — المهام الخلفية دون إدارة دورة الحياة. إطلاق coroutine في نطاق عام دون ربطه بدورة حياة Activity أو ViewModel يؤدي إلى تسريبات: تستمر المهمة في التنفيذ بعد تدمير الشاشة. في Android، استخدم lifecycleScope (Activity/Fragment) أو viewModelScope (ViewModel). في iOS، استخدم weak self في الإغلاقات وألغِ المهام عند deinit.

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

ما هو Background Thread في التطبيقات المحمولة؟

Background Thread — خيط يتم فيه تنفيذ العمليات غير المرتبطة بـ UI: طلبات الشبكة، قراءة/كتابة الملفات، تحليل JSON، الحسابات. يحرر Main Thread من العمل الثقيل، محافظًا على استجابة الواجهة. في iOS، تُدار خيوط الخلفية عبر GCD (DispatchQueue.global)؛ في Android، عبر Executors أو Kotlin Coroutines (Dispatchers.IO، Dispatchers.Default).

ما الفرق بين Dispatchers.IO و Dispatchers.Default؟

Dispatchers.IO مصمم لعمليات الإدخال/الإخراج المحظورة: قراءة الملفات، طلبات الشبكة، العمل مع قاعدة البيانات. لديه مجمع من 64 خيطًا. Dispatchers.Default — للمهام المكثفة للمعالج: الفرز، التصفية، معالجة الصور. مجمعه يساوي عدد أنوية المعالج. استخدام Dispatchers.Default لعمليات IO قد يحظر جميع الأنوية، واستخدام Dispatchers.IO لمهام المعالج قد يخلق عددًا مفرطًا من الخيوط.

كيف يتم التبديل إلى خيط خلفي في iOS؟

DispatchQueue.global(qos: .background).async { } يرسل كتلة إلى قائمة الانتظار الخلفية العامة. بعد اكتمال العمل الخلفي، يجب العودة إلى الخيط الرئيسي عبر DispatchQueue.main.async { } لتحديث UI. للمهام الخلفية التسلسلية، استخدم OperationQueue مع maxConcurrentOperationCount = 1 أو DispatchQueue(label: "serial").

كم عدد خيوط الخلفية التي يمكن إنشاؤها في تطبيق محمول؟

العدد الموصى به من خيوط الخلفية يساوي عدد أنوية المعالج زائد 1 للمهام المرتبطة بـ IO. على جهاز حديث بثمانية أنوية، هذا 9 خيوط. إنشاء المئات من الخيوط يؤدي إلى thread starvation: ينفق نظام التشغيل وقتًا في تبديل السياق أكثر من تنفيذ المهام. يعمل GCD في iOS و Executors في Android على تحسين مجمع الخيوط تلقائيًا للجهاز الحالي.

هل أحتاج للعودة إلى Main Thread بعد coroutine؟

في Kotlin Coroutines، العودة إلى Main Thread تحدث تلقائيًا إذا تم إطلاق coroutine في نطاق Main (lifecycleScope.launch، viewModelScope.launch). دالة withContext(Dispatchers.IO) تعلق coroutine على خيط IO، وبعد الاكتمال تستأنف تلقائيًا على الموزع الذي تم إطلاقها منه (عادة Main). لا حاجة لاستدعاء صريح لـ DispatchQueue.main.async.

الملخص

  • Background Thread — خيط للعمليات التي لا يجب تنفيذها على Main Thread: الشبكة، الملفات، تحليل JSON، الحسابات
  • iOS: DispatchQueue.global(qos:) و OperationQueue هما واجهتا API الرئيسيتان للمهام الخلفية مع دعم QoS
  • Android: Executors، HandlerThread، WorkManager لـ Java؛ Dispatchers.IO/Default + coroutines لـ Kotlin
  • Coroutines مع withContext تبدل الخيط دون callbacks ودون حظر (آلية suspend)
  • WorkManager يضمن تنفيذ المهام الخلفية حتى بعد إعادة تشغيل الجهاز، مع احترام القيود
  • الأخطاء: إنشاء Threads جديدة بدلاً من استخدام مجمع، حالات سباق عند الوصول إلى حالة قابلة للتغيير، تسريبات بسبب عدم الربط بدورة الحياة
  • النتيجة من خيط الخلفية تُعاد دائمًا إلى Main Thread: عبر Dispatchers.Main (Android) أو DispatchQueue.main.async (iOS)

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

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

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

اقرأ أيضًا