suspend function: ما هي، الصيغة والعمل في coroutines

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

Suspend function هي دالة مع المعدل suspend يمكنها إيقاف تنفيذها مؤقتاً دون حظر الخيط واستئنافه لاحقاً في نفس coroutine. وفقاً لـ JetBrains Kotlin Docs, 2025، دوال الإيقاف هي لبنة أساسية في coroutines، توفر عدم التزامن دون استدعاءات رد. يتم ترجمة كل دالة إيقاف إلى آلة حالة تعتمد على Continuation، مما يسمح بإدارة فعالة لنقاط الإيقاف.

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

  • Suspend — كلمة مفتاحية في Kotlin تميز الدالة كقابلة للإيقاف (غير متزامنة)
  • Continuation — معامل مخفي يضيفه المترجم إلى كل دالة إيقاف لحفظ الحالة
  • نقاط الإيقاف — أماكن استدعاء دوال الإيقاف الأخرى، حيث يمكن لـ coroutine التوقف دون حظر
  • آلة الحالة — التمثيل الداخلي لدالة الإيقاف، حيث كل نقطة إيقاف هي حالة منفصلة
  • الاستدعاء فقط من coroutine — يمكن استدعاء دوال الإيقاف فقط من دالة إيقاف أخرى أو من launch/async

ما هي suspend function في Kotlin؟

Suspend function هي دالة تم التصريح عنها باستخدام الكلمة المفتاحية suspend والتي يمكنها إيقاف التنفيذ مؤقتاً في نقطة أو أكثر دون حظر الخيط. كل استدعاء لدالة إيقاف داخل دالة إيقاف أخرى هو نقطة إيقاف محتملة.

kotlin
suspend fun fetchUserData(): User {
    val response = httpClient.get("/user")
    return parser.parse(response)
}

يقوم مترجم Kotlin بترجمة هذه الدالة إلى آلة حالة. كل نقطة إيقاف (استدعاء دالة إيقاف أخرى) تصبح حالة (label). يتم تحرير الخيط الحالي بين الحالات، وبعد اكتمال العملية المنتظرة، يستأنف التنفيذ من الحالة التالية.

تاريخ الظهور

ظهرت دوال الإيقاف في Kotlin 1.3 (2018) مع coroutines كميزة تجريبية وأصبحت مستقرة في Kotlin 1.5 (2021). قبل ذلك، كان يتم تحقيق عدم التزامن في Kotlin/Java من خلال الاستدعاءات الراجعة وRxJava وCompletableFuture. قدمت دوال الإيقاف بديلاً بصيغة خطية وإدارة تلقائية للخيوط.

كيف تعمل دوال الإيقاف: Continuation وآلة الحالة

فهم آلية العمل الداخلية لدوال الإيقاف هو مفتاح العمل الصحيح مع coroutines. على عكس الدوال العادية، يتم ترجمة كل دالة إيقاف إلى فئة مع واجهة Continuation.

Continuation — المعامل المخفي

يضيف مترجم Kotlin معاملاً من نوع Continuation في نهاية قائمة معاملات كل دالة إيقاف. يحتوي Continuation على:

  • context — CoroutineContext (الموزع، المهمة، عناصر السياق)
  • resumeWith — طريقة لاستئناف التنفيذ بنتيجة أو استثناء
  • label — فهرس الحالة الحالية في آلة الحالة

مثال على آلة الحالة

لنفترض أن لدينا دالة إيقاف مع استدعاءين لدوال إيقاف أخرى:

kotlin
suspend fun process() {
    val a = stepOne()
    val b = stepTwo(a)
    println(b)
}

يقوم المترجم بتحويلها إلى آلة حالة مع تسميات:

kotlin
// Simplified generated code representation
fun process(cont: Continuation<Unit>): Any? {
    val cont = cont as ProcessContinuation
    when (cont.label) {
        0 -> {
            cont.label = 1
            if (stepOne(cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
        }
        1 -> {
            cont.label = 2
            val a = cont.result as TypeA
            if (stepTwo(a, cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
        }
        2 -> {
            println(cont.result)
            Unit
        }
    }
}

ملاحظة رئيسية: إذا أعادت الدالة COROUTINE_SUSPENDED، يتم تحرير الخيط الحالي. عندما تكتمل العملية غير المتزامنة، يتم استدعاء Continuation.resumeWith، وتستمر آلة الحالة من التسمية التالية.

صيغة دوال الإيقاف: التصريح والاستدعاء

التصريح عن دالة إيقاف لا يختلف عن الدالة العادية، باستثناء الكلمة المفتاحية suspend قبل fun. هناك قيد واحد فقط: يمكن استدعاء دالة الإيقاف فقط من coroutine أو من دالة إيقاف أخرى.

التصريح الأساسي

kotlin
suspend fun delayAndReturn(ms: Long): String {
    delay(ms)
    return "Done after ${ms}ms"
}

في هذا المثال، delay هي أيضاً دالة إيقاف توقف coroutine مؤقتاً لعدد محدد من المللي ثانية دون حظر الخيط. بعد التأخير، يستأنف التنفيذ.

الاستدعاء من coroutine

kotlin
fun main() = runBlocking {
    val result = delayAndReturn(1000)
    println(result)
}

يقوم runBlocking بإنشاء جسر بين العالم العادي و coroutines. داخل lambda، يمكن استدعاء أي دوال إيقاف.

Lambdas الإيقاف والأنواع الوظيفية

يدعم Kotlin إصدارات الإيقاف للأنواع الوظيفية — suspend () -> T و suspend (A) -> B. هذا يسمح بتمرير lambdas غير متزامنة إلى دوال ذات ترتيب أعلى:

kotlin
suspend fun  withRetry(
    retries: Int = 3,
    block: suspend () -> T
): T {
    repeat(retries - 1) {
        try { return block() }
        catch (_: Exception) { delay(100) }
    }
    return block()
}

تستقبل الدالة withRetry lambda إيقاف وتعيد محاولة تنفيذها عند الأخطاء. هذا نمط نموذجي لطلبات الشبكة مع إعادة المحاولة.

كيف تختلف دوال الإيقاف عن الدوال العادية

الاختلافات بين دوال الإيقاف والدوال العادية تتجاوز مجرد إضافة معدل. دعنا ننظر إلى الاختلافات الرئيسية.

الخاصيةالدالة العاديةدالة الإيقاف
خيط التنفيذتحظر الخيط حتى الاكتماليمكنها تحرير الخيط والاستئناف لاحقاً
معاملات المترجمفقط المعاملات المحددةContinuation ضمني في النهاية
الاستدعاء من دالة عاديةنعملا
المكدسمكدس الخيط الفعليآلة حالة في heap + مكدس فعلي بين النقاط
قيمة الإرجاعقيمة مباشرةقيمة أو COROUTINE_SUSPENDED
الأداءتكلفة إضافية ضئيلة~بضع نانوثانية لكل آلة حالة (Kotlin 1.9+)

لماذا لا يمكن استدعاء دوال الإيقاف من الدوال العادية

الدالة العادية لا تحتوي على Continuation — ليس لديها مكان لحفظ الحالة ولا شيء لاستئناف التنفيذ به. إذا كنت بحاجة لاستدعاء دالة إيقاف من دالة عادية، استخدم runBlocking (للاختبارات) أو CoroutineScope.launch (للإنتاج مع مراعاة دورة الحياة).

أمثلة على دوال الإيقاف في Android

دعنا ننظر إلى ثلاثة سيناريوهات واقعية لاستخدام دوال الإيقاف في تطبيقات Android باستخدام Kotlin.

مثال 1: Room DAO مع استعلامات الإيقاف

يدعم Room دوال الإيقاف مباشرة — يتم تنفيذ الاستعلام في خلفية الخيط تلقائياً:

kotlin
@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUser(id: Int): User?

    @Insert
    suspend fun insertUser(user: User)
}

يستخدم Room داخلياً Dispatchers.IO لتنفيذ الاستعلام، ويتم إرجاع النتيجة في الموزع الذي تم استدعاء دالة الإيقاف عليه.

مثال 2: تركيب دوال الإيقاف لتحميل الشاشة

kotlin
class ProfileViewModel : ViewModel() {
    private val repo = ProfileRepository()

    fun loadProfile(id: String) {
        viewModelScope.launch {
            val profile = repo.getProfile(id)
            _profile.value = profile
        }
    }
}

يقوم ViewModelScope.launch بإنشاء coroutine، يتم داخلها استدعاء دالة الإيقاف getProfile. بعد الحصول على النتيجة، يتم تحديث واجهة المستخدم على الخيط الرئيسي.

مثال 3: خطوات غير متزامنة متسلسلة

kotlin
suspend fun placeOrder(cart: Cart): OrderResult {
    val validated = validateCart(cart)
    val payment = processPayment(validated)
    val receipt = sendReceipt(payment)
    return receipt
}

يتم تنفيذ ثلاث دوال إيقاف بشكل متسلسل. في كل خطوة، يمكن لـ coroutine التوقف دون حظر الخيط. إذا ألقت أي خطوة استثناءاً، لا يتم تنفيذ الخطوات المتبقية، مما يحمي من حالات الطلب غير الصحيحة.

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

حتى مطوري Kotlin ذوي الخبرة يرتكبون أخطاء عند تصميم دوال الإيقاف. دعنا ننظر إلى الأكثر شيوعاً.

الخطأ 1: استدعاءات الحظر داخل الإيقاف

دالة الإيقاف لا تجعل الكود غير متزامن تلقائياً. Thread.sleep() و InputStream.read() والاستدعاءات الأخرى المحظورة ستستمر في حظر الخيط. استخدم withContext(Dispatchers.IO) لتغليف العمليات المحظورة.

الخطأ 2: إنشاء دوال إيقاف دون حاجة

إذا كانت الدالة لا تستدعي دوال إيقاف أخرى ولا تقوم بعمليات غير متزامنة — فإن معدل الإيقاف زائد عن الحاجة. يضيف تكلفة إضافية لآلة الحالة ويقيد سياق الاستدعاء. اجعل الدالة إيقاف فقط عندما تتوقف فعلاً.

الخطأ 3: تجاهل CancellationException

عند إلغاء coroutine، ترمي دوال الإيقاف CancellationException. لا تلتقطها بدون تفكير — أنت تحرم الكود المستدعي من القدرة على إكمال الإلغاء بشكل صحيح. إذا كنت بحاجة لتنفيذ عملية إنهاء، استخدم كتلة finally و NonCancellable.

kotlin
suspend fun safeOperation() {
    try {
        doWork()
    } finally {
        withContext(NonCancellable) {
            cleanup()
        }
    }
}

يتم تنفيذ كتلة finally دائماً، بما في ذلك عند الإلغاء، ويضمن NonCancellable عدم مقاطعة التنظيف.

الخطأ 4: استدعاء دوال الإيقاف من الاستدعاءات الراجعة

لا يمكنك استدعاء دالة إيقاف مباشرة من استدعاء راجع دون إنشاء coroutine. استخدم suspendCoroutine أو suspendCancellableCoroutine لتكييف نمط الاستدعاء الراجع مع coroutines.

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

هل يمكن أن لا تحتوي دالة الإيقاف على نقاط إيقاف؟

نعم، تقنياً يمكن لدالة الإيقاف ألا تستدعي دوال إيقاف أخرى. سيقوم المترجم بإنشاء آلة حالة بحالة واحدة (label 0). ومع ذلك، لا فائدة عملية من مثل هذه الدالة — يتم تنفيذها كدالة عادية ولكن بتكلفة إضافية. لا تستخدم الإيقاف دون حاجة.

كيفية تصحيح أخطاء دوال الإيقاف؟

يوفر Kotlin kotlinx-coroutines-debug — مكتبة مع DebugProbes وأدوات تتبع coroutines. في Android Studio بدءاً من Arctic Fox، هناك علامة تبويب Coroutines مدمجة في المصحح تظهر coroutines النشطة وحالتها ونقاط الإيقاف.

هل يؤثر عدد نقاط الإيقاف على الأداء؟

كل نقطة إيقاف تنشئ حالة جديدة في آلة الحالة. لمعظم التطبيقات، التكلفة الإضافية لنقطة واحدة هي بضع نانوثانية (Kotlin 1.9+). فقط مع عشرات الآلاف من النقاط في حلقة يجب أن تفكر في دمج العمليات أو استخدام sequence/flow.

كيف تختلف دالة الإيقاف عن async/await في اللغات الأخرى؟

في Kotlin، suspend هو معدل لنوع الدالة، وليس علامة قيمة إرجاع (مثل async في C#). يمكن لأي دالة إيقاف أن تأخذ أي معاملات ونوع إرجاع، واستدعاؤها نحوياً لا يختلف عن استدعاء دالة عادية — لا يوجد عامل await في مكان الاستدعاء.

كيفية تحويل دالة استدعاء راجع إلى إيقاف؟

استخدم suspendCancellableCoroutine للتكييف. داخلها، تقوم بتسجيل استدعاء راجع يستدعي continuation.resume()، وتعيد رمز إلغاء إذا كان الاستدعاء الراجع يدعم إلغاء الاشتراك. هذا هو النمط القياسي لتغليف واجهات Android القديمة.

الخلاصة

  • Suspend function — دالة مع معدل الإيقاف يمكنها إيقاف التنفيذ دون حظر خيط عبر آلية Continuation
  • آلة الحالة — التمثيل الداخلي لدالة الإيقاف في bytecode Kotlin، حيث كل نقطة إيقاف هي حالة منفصلة بتسمية
  • Continuation — معامل مخفي يحتوي على سياق coroutine وطريقة resumeWith لاستئناف التنفيذ
  • الاستدعاء فقط من coroutine — دوال الإيقاف غير متاحة من الدوال العادية دون runBlocking أو CoroutineScope
  • العمليات المحظورة داخل الإيقاف تتطلب withContext(Dispatchers.IO) — وإلا يتم حظر الخيط
  • Room و Retrofit يدعمان دوال الإيقاف بشكل أصلي، ويديران تلقائياً خيوط الخلفية
  • CancellationException — تعامل مع الإلغاء عبر finally + NonCancellable، لا تلتقط CancellationException بدون تفكير

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

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

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

اقرأ أيضًا