CoroutineScope — ما هو، دورة الحياة والعمل في الكوروتينات

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

CoroutineScope هي واجهة Kotlin تحدد دورة حياة الكوروتين وتوفر سياقاً لتشغيل كوروتينات جديدة. وفقاً لوثائق Kotlin، 2025، يحتوي كل مثيل CoroutineScope على CoroutineContext ويدير جميع الكوروتينات التي تعمل بداخله. عند إلغاء الـ scope، يتم إلغاء جميع الكوروتينات الفرعية تلقائياً، مما يمنع تسرب الذاكرة.

الخلاصة

  • CoroutineScope — واجهة بحقل CoroutineContext واحد، تحدد دورة حياة الكوروتينات
  • Job — عنصر سياق مسؤول عن الإلغاء: إلغاء الـ scope يلغي جميع الكوروتينات الفرعية
  • التزامن الهيكلي — المبدأ الذي يربط الكوروتينات الفرعية بالـ scope الأب
  • GlobalScope — scope على مستوى التطبيق بالكامل غير موصى به بسبب خطر تسرب الذاكرة
  • supervisorScope — scope خاص لا يؤدي إلغاء كوروتين فرعي فيه إلى إلغاء البقية

ما هو CoroutineScope في Kotlin؟

CoroutineScope هي واجهة أساسية من مكتبة kotlinx.coroutines تعمل كحاوية للكوروتينات. تحدد حدود دورة حياة الكوروتينات: عندما يكتمل الـ scope، يتم إلغاء جميع الكوروتينات داخله تلقائياً.

kotlin
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

تحتوي الواجهة على حقل واحد فقط — coroutineContext. من خلاله، يوفر الـ scope مُوزعاً (Dispatcher)، ومهماً (Job)، ومعالج استثناءات، وعناصر سياق أخرى لجميع الكوروتينات التي تعمل داخله.

الدور في مكتبة kotlinx.coroutines

جميع دوال تشغيل الكوروتينات — launch، async، runBlocking — هي دوال امتداد على CoroutineScope. وهذا يعني أنه لا يمكن استدعاؤها إلا عند وجود كائن scope. يضمن هذا التصميم أن لكل كوروتين أباً ودورة حياة محددة بوضوح.

أين يُستخدم CoroutineScope

في Android، لكل مكون معماري scope خاص به: viewModelScope لـ ViewModel، lifecycleScope لـ Activity/Fragment. في تطبيقات الخادم، يمكن ربط scope بطلب HTTP أو بمجموعة اتصالات قاعدة البيانات.

كيف يعمل CoroutineScope: Job والتزامن الهيكلي

فهم آلية عمل CoroutineScope الداخلية يتطلب الإلمام بمفهوم Job ومبدأ التزامن الهيكلي.

Job — مهمة الكوروتين

يعيد كل كوروتين عند تشغيله كائن Job (أو Deferred لـ async). يمثل Job مهمة ذات دورة حياة محدودة: New، Active، Completing، Completed، Cancelling، Cancelled. تشكل كائنات Job بنية شجرية:

  • Job الأب — الـ scope الذي شُغِّل فيه الكوروتين
  • Job الابن — كل كوروتين شُغِّل عبر launch/async
  • إلغاء الأب → إلغاء جميع الأبناء
  • استثناء في ابن → إلغاء الأب (باستثناء supervisorScope)

مبدأ التزامن الهيكلي

التزامن الهيكلي هو مبدأ معماري رئيسي في Kotlin Coroutines، حيث ترتبط دورة حياة الكوروتين بدورة حياة الـ scope الخاص به. يتناقض هذا مع نموذج “أطلق وانسى”، حيث يستمر الكوروتين في الحياة بعد اكتمال الـ scope. مزايا التزامن الهيكلي:

  • دورة حياة قابلة للتنبؤ — عندما يكتمل الـ scope، يتم ضمان إيقاف جميع الكوروتينات
  • معالجة تلقائية للأخطاء — ينتشر الاستثناء في أي كوروتين فرعي إلى الـ scope
  • لا تسرب للذاكرة — لا يبقى أي كوروتين قيد التشغيل بعد اكتمال الـ scope
  • تسلسل هرمي واضح — يعكس الكود البنية المنطقية للعمليات المتوازية

دورة حياة CoroutineScope

عند استدعاء scope.cancel()، ينتقل Job الخاص بالـ scope إلى الحالة Cancelled، مما يلغي بشكل متكرر جميع Jobs الأبناء. بعد الإلغاء، لا يمكن إعادة استخدام الـ scope إلا إذا تم إنشاء مثيل CoroutineScope جديد.

إنشاء وتكوين CoroutineScope

يمكنك إنشاء CoroutineScope عبر دالة مصنع أو عبر تنفيذ الواجهة في صفك. دعنا نستعرض كلا النهجين.

دالة المصنع CoroutineScope()

kotlin
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

scope.launch {
    println("Running on ${Thread.currentThread().name}")
}

تأخذ دالة المصنع CoroutineContext وتنشئ scope بالسياق المحدد. يستخدم المثال Dispatchers.Default للمهام كثيفة المعالجة وSupervisorJob الذي يعزل الاستثناءات بين الكوروتينات الفرعية.

تنفيذ الواجهة عبر التركيب

kotlin
class MyRepository {
    private val scope = CoroutineScope(Dispatchers.IO + Job())

    suspend fun fetchData(): Data = scope.async {
        api.getData()
    }.await()

    fun cleanup() {
        scope.cancel()
    }
}

نخزن الـ scope كحقل صف ونستدعي يدوياً cleanup لإلغائه. هذا النهج مناسب للمكونات ذات دورة حياة مُدارة — على سبيل المثال، المستودعات أو المدراء.

التنفيذ عبر التفويض

يسمح Kotlin بتفويض تنفيذ CoroutineScope عبر الكلمة المفتاحية by:

kotlin
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
    fun load() {
        launch {
            // coroutine runs in DataLoader scope
        }
    }
}

هذا النهج مناسب عندما يكون الصف نفسه scope ويريد توفير طرق تشغيل الكوروتينات. ولكن كن حذراً: يرث الصف جميع طرق CoroutineScope، بما في ذلك cancel، مما قد يخرق التغليف.

GlobalScope مقابل CoroutineScope المخصص

GlobalScope هو CoroutineScope فردي (singleton) للتطبيق بالكامل. استخدامه في كود الإنتاج غير موصى به رسمياً.

مشاكل GlobalScope

  • غياب التزامن الهيكلي — الكوروتينات في GlobalScope غير مرتبطة بدورة حياة المكون
  • تسرب الذاكرة — قد يستمر الكوروتين في العمل بعد إغلاق Activity/Fragment
  • صعوبة الاختبار — لا يمكن استبدال GlobalScope في الاختبارات
  • استهلاك غير متحكم به للموارد — قد تعمل العديد من الكوروتينات لفترة أطول من المتوقع

متى يكون GlobalScope مبرراً

تسمح JetBrains باستخدام GlobalScope فقط في السيناريوهات النادرة: العمليات الخلفية على مستوى التطبيق التي يجب أن تستمر حتى بعد إغلاق جميع الـ Activities (مثل مزامنة البيانات، التحليلات). ولكن حتى في هذه الحالات، يُفضل إنشاء scope خاص بك باستخدام CoroutineScope(SupervisorJob()).

توصية

استخدم دائماً CoroutineScope مخصصاً مع إدارة دورة حياة واضحة. في Android، هذه هي viewModelScope وlifecycleScope. في تطبيقات الخادم، أنشئ scope لكل طلب أو مجموعة اتصالات.

coroutineScope مقابل supervisorScope: ما الفرق

كلتا الدالتين هما دالتان معلقتان (suspend) تنشئان scope مؤقتاً للمهام المتوازية، لكن سلوكهما مع الاستثناءات يختلف جوهرياً.

الخاصيةcoroutineScopesupervisorScope
السلوك عند الخطأاستثناء في كوروتين فرعي يلغي جميع الآخريناستثناء في كوروتين فرعي لا يلغي الآخرين
نشر الخطأنعم، يتم نشر أول استثناء إلى الخارجنعم، يتم نشر أول استثناء إلى الخارج
Job الافتراضيJob() — الأبناء مرتبطون بالأبSupervisorJob() — الأبناء لا يعتمدون على بعضهم
حالة الاستخدام النموذجيةعملية ذرية متعددة الخطواتمهام متوازية مستقلة (تحميلات UI)

متى تختار coroutineScope

استخدم coroutineScope عندما تشكل عدة عمليات متوازية عملية ذرية واحدة. على سبيل المثال، تحميل بيانات من ثلاثة خوادم: إذا فشل طلب واحد، لا معنى للبقية.

kotlin
suspend fun loadProductPage(): ProductPage = coroutineScope {
    val product = async { api.getProduct() }
    val reviews = async { api.getReviews() }
    ProductPage(product.await(), reviews.await())
}

إذا ألقى getProduct أو getReviews استثناءً — يتم إلغاء كلا الكوروتينين، وينتشر الاستثناء إلى الكود المستدعي.

متى تختار supervisorScope

استخدم supervisorScope عندما لا تعتمد العمليات المتوازية على بعضها. على سبيل المثال، تحميل بيانات الملف الشخصي في عدة أقسام مستقلة: إذا فشل قسم التوصيات، يجب أن يظهر رأس الملف الشخصي وقائمة الأصدقاء.

الأخطاء الشائعة عند العمل مع CoroutineScope

دعنا نستعرض الأخطاء الأكثر شيوعاً للمطورين عند استخدام CoroutineScope في Kotlin.

الخطأ 1: نسيان إلغاء الـ scope

السيناريو الأكثر شيوعاً لتسرب الكوروتينات هو إنشاء scope دون استدعاء cancel عند انتهاء المكون. إذا لم يتم إلغاء الـ scope، تستمر الكوروتينات في العمل، مبقية المراجع على الكائنات. في Android، استخدم viewModelScope أو lifecycleScope، والتي تُلغى تلقائياً.

الخطأ 2: استخدام GlobalScope في Activity أو Fragment

يتجاهل GlobalScope دورة حياة مكونات Android. كوروتين شُغِّل في GlobalScope بعد إغلاق Activity سيستمر في التنفيذ وسيحاول تحديث UI — مما يؤدي إلى تعطل. استخدم دائماً lifecycleScope لمكونات UI.

الخطأ 3: إعادة استخدام scope ملغى

بعد استدعاء cancel()، لا يمكن إعادة استخدام الـ scope — جميع الكوروتينات داخله قد اكتملت بالفعل. أنشئ مثيل CoroutineScope جديداً عبر دالة المصنع. Job() لا يدعم إعادة التنشيط.

الخطأ 4: التفويض غير الصحيح لواجهة CoroutineScope

عند التفويض بـ by، يحصل الصف على طريقة cancel() عامة يمكن استدعاؤها من أي مكان، مما يخرق التغليف. خزّن الـ scope كحقل خاص بدلاً من تفويض الواجهة.

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

ما الفرق بين CoroutineScope و CoroutineContext؟

CoroutineScope هي واجهة تمتلك CoroutineContext وتكون مسؤولة عن دورة حياة الكوروتينات. CoroutineContext هو مجموعة من العناصر (الموزع، job، معالج الأخطاء) تحدد “كيف” يتم تنفيذ الكوروتين. أحد الفروق: الـ scope ينشئ الكوروتينات، بينما يتحكم السياق في سلوكها.

هل يمكن إنشاء CoroutineScope باستخدام SupervisorJob؟

نعم، هذا نمط قياسي: CoroutineScope(Dispatchers.IO + SupervisorJob()). يمنع SupervisorJob الإلغاء المتسلسل للكوروتينات الفرعية عند حدوث استثناء في أحدها. هذا مفيد للمهام المتوازية المستقلة حيث لا يجب أن يوقف خطأ في إحداها الأخرى.

كم عدد الكوروتينات التي يمكن أن يحتويها CoroutineScope؟

لا يوجد حد لعدد الكوروتينات في scope — فهي محدودة فقط بالذاكرة المتاحة وإعدادات الموزع. الحد العملي عادة هو آلاف الكوروتينات النشطة في scope واحد. ومع ذلك، قد يشير العدد الكبير من الكوروتينات إلى مشاكل معمارية.

كيف تختبر الكود مع CoroutineScope؟

الطريقة الصحيحة هي تمرير الـ scope إلى الصف عبر المُنشئ أو استخدام runBlockingTest / runTest من kotlinx-coroutines-test. في الاختبارات، يمكنك استبدال الـ scope بـ TestCoroutineDispatcher والتحكم في تنفيذ الكوروتينات يدوياً.

هل يمكن أن يكون للكوروتين scope خاص به؟

لا، الـ scope هو حاوية خارجية للكوروتين. الكوروتين نفسه ليس scope. ومع ذلك، داخل الكوروتين يمكنك إنشاء scope جديد عبر coroutineScope أو supervisorScope لتشغيل كوروتينات فرعية بالتوازي.

الملخص

  • CoroutineScope — واجهة بحقل coroutineContext، تحدد دورة حياة الكوروتينات التي تعمل بداخله
  • التزامن الهيكلي — إلغاء الـ scope يلغي تلقائياً جميع الكوروتينات الفرعية، مما يمنع تسرب الذاكرة
  • Job و SupervisorJob — وضعان لمعالجة الأخطاء: الإلغاء المتسلسل (Job) والأخطاء المعزولة (SupervisorJob)
  • GlobalScope — غير موصى به للإنتاج بسبب عدم الارتباط بدورة الحياة
  • coroutineScope مقابل supervisorScope — عمليات متوازية ذرية مقابل مهام متوازية مستقلة
  • viewModelScope و lifecycleScope — scopes جاهزة لنظام Android، تُلغى تلقائياً عند انتهاء المكون
  • دالة المصنع — الطريقة المفضلة لإنشاء scope عبر CoroutineContext + استدعاء cancel صريح

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

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

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

اقرأ أيضًا