الدوال التأجيلية Suspended Functions

Mohammad Laifمنذ 4 سنوات

بسم الله الرحمن الرحيم

السلام عليكم ورحمة الله وبركاته

 

في هذا الدرس سنتعرف على الدوال التأجيلية التي تقوم على مبدأ التأجيل. فهي تعتبر صلب الـ Kotlin Corotuines ونستطيع تشبيهها على أنها طريقة مختصرة لإنشاء الكروتينات او بالاحرى تغليفهم Encapsulating بداخل الدوال! ايضاً سنقوم بإستخدام جميع ماجاء في الدروس السابقة كالنطاق Scope والبناء Builder والموزع Dispatcher لإنشاء الكروتينات لفهم هذه الدوال.

 

ماذا ستقرئ في هذا الدرس

  • مواصفات الدوال التأجيلية.
  • نظرة على الدروس السابقة تساعد على فهم الدوال التأجيلة.
  • ما هي الدوال التأجيلية في لغة الكوتلن.
  • إنشاء دالة تأجيلية.
    • تشغيل الدالة.
    • تشغيل محتوى الدالة في خيط حاسوبي محدد أو نقلها من خيط الى آخر.
  • التزامن في الدوال التأجيلية.
  • التشغيل في وضع تسلسلي Sequential متزامن في التوقيت Synchronous.
  • التشغيل في وضع تنافسي Concurrent غير متزامن في التوقيت Asynchronous.
  • نمط كتابة الدوال التنافسية.
  • الهيكلة في الدوال التأجيلية.
    • إستخدام نطاق الـ coroutineScope.
    • إستخدام نطاق الـ supervisorScope.

 

ضع في الحسبان انك لا نستطيع نداء واستخدام الدوال التأجيلية الا من خلال كروتين أو من دوال تأجيله اخرى.

 

مواصفات الدوال التأجيلية

  • نستطيع ايقافها واستئنافها لاحقاً.
  • نستطيع تشغيلها لفتره طويله وانتظار النتيجه بدون ان تتسبب في عمل حجب للخيط الحاسوبي.
  • نستطيع ندائها فقط من دوال تأجيلية مثلها أو من داخل كروتينات.
  • نستطيع تشغيلها بشكل تنافسي Concurrent بإستخدام الدالة async.
  • كذلك عند استخدام الدالة async تعطينا ناتج يتمثل في Differed او ترمي بخطئ Expecion اذا لم تكمل او حدث شئ.

 

نظرة على الدروس السابقة تساعد على فهم الدوال التأجيلة

من الدرس السابق بعنوان نظرة عامة على الـ Coroutines نستطيع فهم مبدأ التأجيل كالتالي:

لو فرضنا أن لدينا روتينان (تستطيع تخيلهما كدالتان) يستطيع الروتين الأول التعاون مع الروتين الثاني في أداء المهمة. وذلك لأن الروتين يستطيع أيقاف تنفيذ نفسه في أي سطر برمجي, ولديه القدرة على حفظ رقم ذلك السطر والحالة التي وصل لها في أداء المهمة. ثم يعطي المهمة الى الروتين الآخر ليبدء عمله, وهكذا دواليك. أي أن الروتينات لا يجب عليها أن تطأ موطئ العبارة return في نهايتها.

وهذا يعني بأن الدالة التأجيلية تستطيع تأجيل نفسها مؤقتاً في أي لحظة, لتسمح لأخرى بالعمل, ثم تستطيع استئناف نفسها عندما يأتي دورها.

 

كذلك نستطيع رؤية كيف أن الدوال التأجيلية تعمل على الخيط الحاسوبي من خلال الدرس نظرة خاصة على الـ Coroutines في لغة الكوتلن:

الـ Coroutines يستطيع تأجيل نفسه للسماح بغيرة بإكمال المهمة على الخيط الحاسوبي, ومن ثم يكمل من حيث بدأ.

فهي تقوم بتأجيل نفسها حتى تسمح الأخرى بالعمل على الخيط الحاسوبي, بدون الحاجة الى حجبه وتهنيق البرنامج.

 

وايضاً لاننسى هيكلة النطاقات عندما نتعامل مع الدوال التأجيلية فيجب هيكلت نطاقاتها كذلك, مثل ما جاء في الدرس السابق كما يلي:

إنشاء الكثير من النطاقات (والعديد من الكروتينات بداخلها) بشكل عبثي وبدون ترتيب يؤدي إلى حدوث مشاكل, من بينها تسرب في الذاكرة. لذلك من الافضل ان نقوم بهيكلة النطاقات بشكل جيد, أو استخدام نطاقات خاصة كما جاء ذكرهم سابقاً. كنطاق ViewModelScope الخاص بالـ ViewModel فعندما يغادر المستخدم من الـ Activity التي تحتضن تلك الـ ViewModel فسيغلق ذلك النطاق وجميع الكروتينات سيتم إنهائها بشكل آلي. أو النطاق الخاص بالـ LifeCycle فعندما تنتهي دورة حياة أحد المكونات فإن نطاقها ينتهي معها وينهي جميع الكروتينات به.

ولكن اذا اردنا إنشاء نطاقات خاصة يجب مراعاة الهيكلة لهم, والتفكير في الأمر كعلاقة طفل Child وأب Parent. والاخذ في الحساب إن انواع النطاقات تختلف في تصرفها, فمثلاً الـ coroutineScope النطاق المنشئ من هذه الدالة المؤجله سيلغى اذا فشل احد النطاقات الموجودة بداخلة. إما الـ supervisorScope النطاق المنشئ من هذه الدالة المؤجلة لن يتم إلغاءه إذا فشل أحد النطاقات الموجودة بداخله. إما الـ GlobalScope ينشئ لنا نطاق عام, يبقى نشط دائماً خلال دورة حياة البرنامج, وهذا يعني ان كل الكروتينات التي ننشئها في داخله لن يتم الغاءها حتى لو انتهى عملها فمن الافضل تجنب هذا النوع حتى لانقع في المشاكل.

فمثلاً عندما نستخدم أكثر من نطاق مهيكل من coroutineScope لجلب البيانات من سيرفر ما, إذا فشل أي من الـ request سيتوقف عمل النطاق وهكذا تتوقف جميع الـ requests الموجودة بنطاقات بداخلة. ولتخطي هذه المشكله نستطيع الاستعانه بـ supervisorScope والامر مشابه للعنصر SupervisorJob كذلك.

 

ما هي الدوال التأجيلية في لغة الكوتلن

هي دوال مشابهة في كتابتها مع الدوال الاعتيادية ولكنها تتميز بوضع كلمة suspend في بدايتها. ولها القدرة على إيقاف تنفيذ نفسها مؤقتاً (Suspending). ولها القدرة كذلك على الاستئناف (Resuming). نستطيع جعل تنفيذها يعمل بشكل متزامن (القصد هنا في شكل تنافسي Concurrent كغير متزامن في التوقيت Asynchronous) أو بشكل غير متزامن (القصد هنا في شكل تسلسلي Sequential متزامن في التوقيت Synchronous). وكذلك نستطيع جعلها تقوم بحجب نفسها. ويجب الانتباه الى اننا لانستطيع ندائها الا من خلال دالة اخرى تشابهها, أو من خلال كروتين. وتستطيع هذه الدوال تنفيذ شفرات برمجية لمدة زمنية طويلة وانتظار النتيجة بدون أن تعمل حجب او تعليق للبرنامج (بعكس الخيط الحاسوبي فأن تم حجبه, ينتهي البرنامج). سنرى كل هذه الأمور في الأمثلة القادمة.

 

إنشاء دالة تأجيلية

الشفرة البرمجية التالية توضح لنا طريقة إنشاء هذا النوع من الدوال:

suspend fun foo() {
    
}
  • لاحظ كلمة suspend في البداية.

 

تشغيل الدالة:

fun main() = runBlocking {
    foo()
}

Or

fun main() {
    GlobalScope.launch {
        foo()
    }
}
  • لاحظ جعل الدالة main دالة تأجيلية, حتى نستطيع تشغيل دالتنا foo (تشغيلها من خلال كروتين, وذلك بإستخدام البناء runBlocking).
  • لاحظ في المثال الثاني تشغيلها بإستخدام كروتين كذلك ولكن بإستخدام البناء launch.

 

تشغيل محتوى الدالة في خيط حاسوبي محدد أو نقلها من خيط الى آخر

في الدرس السابق تعرفنا على المرسلين Dispatchers وكانت وظيفتهم هي وضع الروتينية في خيط حاسوبي سواء كان Main أو آخر. هنا في الدوال التأجيلية نستخدم البناء Builder والذي يتمثل في الدالة withContext.

تنفذ شفرتها في الخيط الحاسوبي الرئيسي:

suspend fun searchNewsAPI(query: String) {
    // Main Thread.
}

 

 تنفذ شفرتها في خيط حاسوبي خلفي (بركة من الخيوط الحاسوبية):

suspend fun searchNewsAPI(query: String) {
    withContext(Dispatchers.IO) {
        // Background Thread.
    }
}
  • لاحظ تحديد استخدام نوع المرسل Dispatcher من خلال الدالة withContext (للمزيد عن أنواع الـ Dispatchers راجع درس الموزعات\المرسلين السابق).

 

التزامن في الدوال التأجيلية

في الوضع الاعتيادي defualt فإن الدوال التأجيلية تعمل بشكل تسلسلي متزامن في التوقيت, فهي تقوم بحجب الروتينة Block حتى تنتهي منها ثم تنتقل الى آخرى.

val header = suspend { fetchHeader() }
val article = suspend { fetchArticle(header) }
  • لاحظ أن عملية التنفيذ ستتم بشكل متسلسل. أي ان الدالة الاولى ستنفذ وبعد انتهاء تنفيذها ستبدء الدالة الثانية بالتنفيذ.

 

هذا الشئ قد تعودنا عليه سابقاً (إي التنفيذ التسلسلي) ولا شئ جديد هنا سوى ميزة التأجيل التي تسمح لنا بتشغيل الشفرات بدون تهنيق البرنامج! ولكن ماذا لو اردنا تشغيل الشفرات بشكل متزامن؟ ففي المثال السابق كل ما رأيناه عن طريقة عمل الدوال كان بطريقة تسلسلية و تعاقبية. بمعنى لو كانت لدينا دالتين A و B الأولى تقوم بجلب اسم ومعلومات للشخص من سيرفر API والثانية تقوم بجلب صورة ذلك الشخص. لو قمنا بتشغيلهم بطريقة تسلسلية فأن الصور لن تأتي إلا بعد أن تأتي معلومات الشخص. فلو تطلب جلب المعلومات ٤ ثواني وتطلب جلب صورته ٦ ثواني يصبح المجموع ١٠ ثواني. هنا قمنا بالاستفادة من الكروتينات في تشغيل هذه الاكواد في الخلفية حتى لا تقوم بحجب الخيوط الحاسوبية وتعطيل البرنامج. ولكننا لم نستفد من موارد السيرفر حيث انه يسمح لنا بأكثر من اتصال واحد في الوقت نفسه. وحتى نستفيد من هذا الشئ يجب علينا إستخدام مميزات الكروتينات بجعل الدالتين تعمل في نفس الوقت بشكل تنافسي وذلك بإستخدام البناء async.

 

واقصد هنا هو تشغيل الدالتين بشكل تنافسي Concurrent غير متزامن في التوقيت Asynchronous.

 

التشغيل في وضع تسلسلي Sequential متزامن في التوقيت Synchronous:

  • لاحظ ان الدالة B لن يتم تنفيذها الى بعد ان تنتهي الدالة A من التنفيذ (الدالة B مؤقته للتشغيل بعد الدالة A). 

 

التشغيل في وضع تنافسي Concurrent غير متزامن في التوقيت Asynchronous:

  • لاحظ ان الدالة B سيتم تنفيذها مع الدالة A (الدالة B ليست مؤقته للتشغيل بعد الدالة A).

 

مثال برمجي

لنأخذ المثال في الدرس السابق: لدينا دالتين الاولى بإسم functionA والثانية بإسم functionB يتطلب كل منهما ٤ ثواني للتنفيذ. اذا قمنا بتنفيدهم بشكل تسلسلي وتعاقبي فأننا سنحتاج الى ٨ ثواني:

suspend fun functionA(): Long = measureTimeMillis {
    delay(4000)
}

suspend fun functionB(): Long = measureTimeMillis {
    delay(4000)
}

 

فاذا قمنا بتشغيلهم بشكل تنافسي فإننا سنحتاج فقط الى ٤ ثواني:

runBlocking {
    val functionATime: Deferred<Long> = async { functionA() }
    val functionBTime: Deferred<Long> = async { functionB() }
}

 

اذا كنا نريد نتيجة منه لابد لنا من اخبار الدالة async ان تنتظر للنتيجة بإستخدام الدالة await على العنصر من نوع Deferred المرجع هكذا:

runBlocking {
    val functionATime: Deferred<Long> = async { functionA() }
    val functionBTime: Deferred<Long> = async { functionB() }
    println("functionA: ${functionATime.await()} functionB: ${functionBTime.await()}")
}

 

نمط كتابة الدوال التنافسية

يوجد نمط ينصح به حين التعامل مع الدوال التأجيلية والبناء async. فمثلاً هذه الدالة searchNewsAPI تعتبر دالة تأجيلية وذلك من خلال وضع المعرف suspend لها:

suspend fun searchNewsAPI(query: String) {
    withContext(Dispatchers.IO) {
        // Background Thread.
    }
}

 

وهذا يعني اننا لانستطيع إستخدامها في الدوال الاعتيادية! فلو اردنا استخدامها في الـ main سوف تظهر الرسالة التالية:

Suspend function 'searchNewsAPI' should be called only from a coroutine or another suspend function.

 

لذلك نستطيع تغليفها في داخل دالة اخرى تقوم بتشغيلها بداخل نطاق خاص بها! وهنا يأتي نمط تسمية الدوال التنافسية لتصبح:

fun searchNewsAPIAsync(query: String) = GlobalScope.async {
    searchNewsAPI(query)
}
  • لاحظ طريقة وضع  نطاق Scope للدالة GlobalScope.async.
  • لاحظ طريقة تسمية الدالة الجديدة وذلك بإضافة كلمة Async في نهاية تسميتها.
  • وهكذا نستطيع ندائها من إي دالة عادية. ولكن اذا اردنا الحصول على عنصر الـ Deferred يجب علينا تغليف الشفرة المسؤولة عن الحصول عليه في كروتينه.

 

الهيكلة في الدوال التأجيلية

في الدرس السابق بعنوان النطاقات تعرفنا على نوعين من النطاقات يتمثلان في دالتين تأجيليتين وهما كلاً من الـ coroutineScope و الـ supervisorScope والان سنقوم بإستخدامهما مع الدوال التأجيلية.

إستخدام الـ coroutineScope:

suspend fun getNumberOfArticles() = coroutineScope {

}
  • انتبه الى ان النطاق coroutineScope سيقوم بإلغاء اي كروتين اذا احداها فشلت.

 

إستخدام الـ supervisorScope:

suspend fun getNumberOfAuthors() = supervisorScope {

}
  • هذا النطاق supervisorScope لن يقوم بإلغاء أي كروتين اذا فشل احداها.

 

في هذا الدرس تعرفنا على اساسيات الدوال التأجيلية, رأينا كيفية إنشائهم والنمط المستحسن في ذلك. في الدرس القادم سنتعرف على التيارات Streams وبالاخص القنوات Channels.

 

المصادر

  • كوتلن Doc.

المحاضر

Mohammad Laif

محتوى الدورة

الكلمات الدليلية

عن الدرس

0 إعجاب
1 متابع
0 مشاركة
1339 مشاهدات
منذ 4 سنوات

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !