Suspend function هي دالة مع المعدل suspend يمكنها إيقاف تنفيذها مؤقتاً دون حظر الخيط واستئنافه لاحقاً في نفس coroutine. وفقاً لـ JetBrains Kotlin Docs, 2025، دوال الإيقاف هي لبنة أساسية في coroutines، توفر عدم التزامن دون استدعاءات رد. يتم ترجمة كل دالة إيقاف إلى آلة حالة تعتمد على Continuation، مما يسمح بإدارة فعالة لنقاط الإيقاف.
النقاط الرئيسية
Suspend function هي دالة تم التصريح عنها باستخدام الكلمة المفتاحية suspend والتي يمكنها إيقاف التنفيذ مؤقتاً في نقطة أو أكثر دون حظر الخيط. كل استدعاء لدالة إيقاف داخل دالة إيقاف أخرى هو نقطة إيقاف محتملة.
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. قدمت دوال الإيقاف بديلاً بصيغة خطية وإدارة تلقائية للخيوط.
فهم آلية العمل الداخلية لدوال الإيقاف هو مفتاح العمل الصحيح مع coroutines. على عكس الدوال العادية، يتم ترجمة كل دالة إيقاف إلى فئة مع واجهة Continuation.
يضيف مترجم Kotlin معاملاً من نوع Continuation في نهاية قائمة معاملات كل دالة إيقاف. يحتوي Continuation على:
لنفترض أن لدينا دالة إيقاف مع استدعاءين لدوال إيقاف أخرى:
suspend fun process() {
val a = stepOne()
val b = stepTwo(a)
println(b)
}
يقوم المترجم بتحويلها إلى آلة حالة مع تسميات:
// 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 أو من دالة إيقاف أخرى.
suspend fun delayAndReturn(ms: Long): String {
delay(ms)
return "Done after ${ms}ms"
}
في هذا المثال، delay هي أيضاً دالة إيقاف توقف coroutine مؤقتاً لعدد محدد من المللي ثانية دون حظر الخيط. بعد التأخير، يستأنف التنفيذ.
fun main() = runBlocking {
val result = delayAndReturn(1000)
println(result)
}
يقوم runBlocking بإنشاء جسر بين العالم العادي و coroutines. داخل lambda، يمكن استدعاء أي دوال إيقاف.
يدعم Kotlin إصدارات الإيقاف للأنواع الوظيفية — suspend () -> T و suspend (A) -> B. هذا يسمح بتمرير lambdas غير متزامنة إلى دوال ذات ترتيب أعلى:
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 باستخدام Kotlin.
يدعم Room دوال الإيقاف مباشرة — يتم تنفيذ الاستعلام في خلفية الخيط تلقائياً:
@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 لتنفيذ الاستعلام، ويتم إرجاع النتيجة في الموزع الذي تم استدعاء دالة الإيقاف عليه.
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. بعد الحصول على النتيجة، يتم تحديث واجهة المستخدم على الخيط الرئيسي.
suspend fun placeOrder(cart: Cart): OrderResult {
val validated = validateCart(cart)
val payment = processPayment(validated)
val receipt = sendReceipt(payment)
return receipt
}
يتم تنفيذ ثلاث دوال إيقاف بشكل متسلسل. في كل خطوة، يمكن لـ coroutine التوقف دون حظر الخيط. إذا ألقت أي خطوة استثناءاً، لا يتم تنفيذ الخطوات المتبقية، مما يحمي من حالات الطلب غير الصحيحة.
حتى مطوري Kotlin ذوي الخبرة يرتكبون أخطاء عند تصميم دوال الإيقاف. دعنا ننظر إلى الأكثر شيوعاً.
دالة الإيقاف لا تجعل الكود غير متزامن تلقائياً. Thread.sleep() و InputStream.read() والاستدعاءات الأخرى المحظورة ستستمر في حظر الخيط. استخدم withContext(Dispatchers.IO) لتغليف العمليات المحظورة.
إذا كانت الدالة لا تستدعي دوال إيقاف أخرى ولا تقوم بعمليات غير متزامنة — فإن معدل الإيقاف زائد عن الحاجة. يضيف تكلفة إضافية لآلة الحالة ويقيد سياق الاستدعاء. اجعل الدالة إيقاف فقط عندما تتوقف فعلاً.
عند إلغاء coroutine، ترمي دوال الإيقاف CancellationException. لا تلتقطها بدون تفكير — أنت تحرم الكود المستدعي من القدرة على إكمال الإلغاء بشكل صحيح. إذا كنت بحاجة لتنفيذ عملية إنهاء، استخدم كتلة finally و NonCancellable.
suspend fun safeOperation() {
try {
doWork()
} finally {
withContext(NonCancellable) {
cleanup()
}
}
}
يتم تنفيذ كتلة finally دائماً، بما في ذلك عند الإلغاء، ويضمن NonCancellable عدم مقاطعة التنظيف.
لا يمكنك استدعاء دالة إيقاف مباشرة من استدعاء راجع دون إنشاء coroutine. استخدم suspendCoroutine أو suspendCancellableCoroutine لتكييف نمط الاستدعاء الراجع مع coroutines.
الأسئلة الشائعة
نعم، تقنياً يمكن لدالة الإيقاف ألا تستدعي دوال إيقاف أخرى. سيقوم المترجم بإنشاء آلة حالة بحالة واحدة (label 0). ومع ذلك، لا فائدة عملية من مثل هذه الدالة — يتم تنفيذها كدالة عادية ولكن بتكلفة إضافية. لا تستخدم الإيقاف دون حاجة.
يوفر Kotlin kotlinx-coroutines-debug — مكتبة مع DebugProbes وأدوات تتبع coroutines. في Android Studio بدءاً من Arctic Fox، هناك علامة تبويب Coroutines مدمجة في المصحح تظهر coroutines النشطة وحالتها ونقاط الإيقاف.
كل نقطة إيقاف تنشئ حالة جديدة في آلة الحالة. لمعظم التطبيقات، التكلفة الإضافية لنقطة واحدة هي بضع نانوثانية (Kotlin 1.9+). فقط مع عشرات الآلاف من النقاط في حلقة يجب أن تفكر في دمج العمليات أو استخدام sequence/flow.
في Kotlin، suspend هو معدل لنوع الدالة، وليس علامة قيمة إرجاع (مثل async في C#). يمكن لأي دالة إيقاف أن تأخذ أي معاملات ونوع إرجاع، واستدعاؤها نحوياً لا يختلف عن استدعاء دالة عادية — لا يوجد عامل await في مكان الاستدعاء.
استخدم suspendCancellableCoroutine للتكييف. داخلها، تقوم بتسجيل استدعاء راجع يستدعي continuation.resume()، وتعيد رمز إلغاء إذا كان الاستدعاء الراجع يدعم إلغاء الاشتراك. هذا هو النمط القياسي لتغليف واجهات Android القديمة.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا