إنشاء الكروتين Coroutine

Mohammad Laifمنذ 4 سنوات

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

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

 

في هذا الدرس سنقوم بإنشاء كروتين Coroutine بطريقة يدويه. فبعد الانتهاء من مفهوم الكروتينات في الدروس السابقة بشكل نظري حان الوقت تطبيقه بشكل عملي وذلك بكتابة بعض من الشفرات البرمجية!

 

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

  • إنشاء الـ Coroutine.
  • هيكلية صناعة الكروتين Coroutine.
  • مبدأ التأجيل في الكروتينات.
  • أستخدام الـ Job.
  • الكروتينات خفيفة جداً.
  • هيكلية الكروتينات.

 

في البداية كما جاء في الدروس السابقة:

تم بناء الـ Coroutines في لغة الكوتلن فوق الـ Threads للغة الجافا على شاكلة مكتبة اولية 1st party (اختصار الى First-Party Library) ويعني هذا أنه تم بناؤها من قبل صناع لغة الكوتلن أنفسهم, وليس من طرف مستخدم اللغة الا وهو المبرمج 2nd party, او من طرف آخر خارجي كشركة برمجيات مختلفة 3rd party.

لذلك تحتاج الى اضافة الاعتمادية في مشروعك. من خلال هذه الصفحة انسخ الاعتمادية وقم بإضافتها. انتبه الى نوع مشروعك! فمشاريع الكوتلن ومشاريع الاندرويد لديها أعتماديات مختلفة.

 

يجب الانتباه الى ان بعض الاشياء لاتتوفر في الاعتماديات كلها. فمثلاً لو كان مشروعك كوتلن واضفت اعتمادية الكروتينات للغة الكوتلن فلن تستطيع استخدام الموزع/المرسل Dispatchers بنوع Main. لإنه يوجد فقط في اعتمادية الاندرويد.

 

إنشاء الـ Coroutine

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

اولاً: نحتاج الى نطاق Scope: حتى نصنع الكروتين في داخله. ولحسن الحظ الـ API يوفر لنا العديد ومن بينها نطاق عام بإسم GlobalScope.

GlobalScope

ثانياً: نحتاج الى بناء Builder: حتى يقوم ببنائه. وكذلك الـ API يوفر لنا العديد ومن ابرزها بناء بإسم launch.

GlobalScope.launch

ثالثاً: نحتاج الى مرسل Dispatchers: حتى يأخذه بعد بنائه ويرسله الى احد الخيوط الحاسوبية ويضعه هناك ليتم تنفيذه. وبالطبع يوفر الـ API العديد ومن بينها المسمى الافتراضي.

GlobalScope.launch(Dispatchers.Default)

والان هذا هو كروتينك الاول:

GlobalScope.launch(Dispatchers.Default) {
    // ...
}
  • لاحظ أن النطاق GlobalScope يحتاج إلى بناء launch, والبناء يحتاج الى مرسل Dispatchers.Default.
  • كل ماستكتبه من شفره بداخله سينطبق عليه مبدأ التأجيل.

 

هيكلية صناعة الكروتين Coroutine

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

 

في الدروس القادمة سنتناول الثلاثة الأشياء التي سمحت لنا بصناعة الكروتين. بشكل مفصل درس لكل منها الـ Scope و الـ Builder و الـ Dispatchers. والان لنلعب قليلاً معه! لفهم كيف يعمل هذا الكروتين!

 

مبدأ التأجيل في الكروتينات
من الدرسان السابقين علمنا بإن الكروتين Coroutine يستطيع تأجيل نفسه والسماح للآخرين بالاكمال حتى انتهاء البرنامج, بدلاً من أن يقوم بحجب الخيط الحاسوبي Thread. انظر الى المثال التالي: لدينا كروتين A وكروتين B, كل منهما لديه دالة delay تؤجل الاول لثانية والثاني لثانيتين, ثم تطبع عبارة مكتوبه بداخلهما. ولدينا في البرنامج آمر طباعة ثالث كذلك.

fun main() {

    GlobalScope.launch(Dispatchers.Default) {
        delay(1000L)
        println("Printing from Coroutine A")
    }

    GlobalScope.launch(Dispatchers.Default) {
        delay(2000L)
        println("Printing from Coroutine B")
    }

    println("Printing from the Program")
    
}

 

عند تشغيل البرنامج النتيجة ستكون فقط طباعة العبارة:

Printing from the Program

بدون طباعة العبارات التي بداخل الكروتين A و B. وسبب ذلك يعود إلى أن البرنامج انتهى تقريباً في أقل من ثلاثة أجزاء من الثانية. وتم تأجيل الكروتين A إلى ثانية واحدة و الكروتين الى ثانيتين بإستخدام الدالة delay (بدلاً من أن يقوما بحجب الخيط وإجبار البرنامج على تنفيذهما وطباعة العبارات كما هو الحال مع الدالة sleep الخاصة بالـ Thread). وهكذا قام النطاق GlobalScope بإنهاء جميع الكروتينات المتعلقة به ومن بينها الكروتين A و B.

هنا نلاحظ شيئان:

  1. أن الكروتين A و B لم يقوما بحجب الخيط الحاسوبي لتنفيذ شفراتهما.
  2. وكذلك الدالة delay لم تقم بحجب الخيط الحاسوبي.

بل كلاً من الكروتين والدالة delay قاما بإستخدام مبدأ التأجيل Suspending على مبدأ الحجب Blocking.

 

ماذا نستفيد من هذا الشئ؟

لو ان لدينا شفرة تتطلب وقتاً طويلاً, ووضعناها في كروتين فهكذا لن نصنع حجب في الخيوط الحاسوبية ولا تعليق للبرنامج. فإن كان الوقت كافياً سوف نحصل على النتيجة, ولو كان ضيقاً فسوف تؤجل بدلاً من حجب الخيط وتعليق البرنامج. واذا انتهى زمن النطاق GlobalScope فأنه سيغلق وينهي جميع الكروتينات المتعلقه به.

ماهي الدالة delay؟

الدالة delay تعتبر دالة تأجيلية تم بنائها بداخل الـ API للكروتينات. ولا نستطيع استخدامها إلا بداخل كروتين او بدالة تأجيلية اخرى. سنتعرف على الدوال التأجيلية في دروس لاحقة. ولكن تستطيع فهمها حالياً على أنها كروتين على شاكلة دالة.

 

والآن نستطيع إعطاء وقت اكثر للبرنامج يتعدى الـ ٣ ثواني حتى يقوما الكروتين A و B بالاستئناف وتنفيذ شفراتهما. وذلك بإستخدام الدالة sleep للخيط الحاسوبي.

fun main() {

    GlobalScope.launch(Dispatchers.Default) {
        delay(1000L)
        println("Printing from Coroutine A")
    }

    GlobalScope.launch(Dispatchers.Default) {
        delay(2000L)
        println("Printing from Coroutine B")
    }

    println("Printing from the Program")

    Thread.sleep(3000L)
    
}

ستكون النتيجة هكذا:

Printing from the Program
Printing from Coroutine A
Printing from Coroutine B
  • المترجم (Compiler) قام بقراءة الكروتين A وتأجيله لثانية (Suspending).
  • المترجم (Compiler) قام بقراءة الكروتين B وتأجيله لثانيتين (Suspending).
  • المترجم (Compiler) قام بقراءة السطر المسؤول عن طباعة العبارة Printing from the Program ونفذه فوراً.
  • المترجم قام بقراءة السطر Thread.sleep(3000). وحجب عمل انتهاء البرنامج لينتهي بعد ٣ ثواني.
  • بعد مرور بعض من الوقت أتى دور الكروتين A بعد ثانية واستأنف وطبع ماكتب بداخله. وآتى دور الكروتين B واستأنف بعد ثانيتين وطبع مابه (Resuming).

 

أستخدام الـ Job

ولكن ليس من الجيد استخدام الدالة sleep لتأجيل عمل البرنامج (او بالاحرى تأجيل الخيط الحاسوبي) الى وقت لانستطيع تخمينه. بل من الأفضل تغليف الكروتين بداخل عنصر من نوع Job. ثم تشغيل هذا الـ Job وانتظار النتيجة.

كما بالمثال التالي:

fun main() {

    val jobA: Job = GlobalScope.launch(Dispatchers.Default) {
        delay(1000L)
        println("Printing from Coroutine A")
    }

    val jobB: Job = GlobalScope.launch(Dispatchers.Default) {
        delay(2000L)
        println("Printing from Coroutine B")
    }

    println("Printing from the Program")

    runBlocking {
        jobA.join()
        jobB.join()
    }

}

سنتعرف على الـ Job لاحقاً وكذلك الـ Differed. ولكن اذا كان لديك خلفية في الجافا فماهما الا عبارة عن عناصر Runnable و Callable يحمل في طياته كائن future.

 

والان ماهو هذه الـ Job

بالنسبه لكل من هاؤلاء فسنتعرف عليهم لاحقاً في الدروس, ولكن تستطيع فهمهم الان على ان الـ Job يمثل نوعاً مشابه للـ Runnable إي لايستطيع إرجاع قيمة, ويوجد كذلك نوع آخر منه يسمى بالـ Deferred يمثل نوعاً ما مشابه للـ Callable يستطيع إرجاع قيمة مغلفه (عبارة عن كائن مستقبلي Future). مقالة تتناول مثال بسيط عن الفرق بينهم المشغل و المنادى والكائنات المستقبلية في الاندرويد.

و الـ runBlocking؟

يعتبر بناء Builder يقوم بصناعة نطاق خاص به, ومن مميزات هذا البناء انه يقوم بحجب الخيط الحاسوبي. إي اذا قمت بتشغيل إي كروتين او دالة تأجيلية بداخلة فإنها سوف تقوم بحجب الخيط وتعليق البرنامج الى ان ينتهي عملها, وهذا الشئ ضد مبدأ الكروتينات ويعتبر سيئ جداً. اغلب استخدامات هذا البناء فقط في كتابة وتشغيل الاختبارات, ولتوضيح بعض من الامثله.

والـ Job.join؟

طريقة لتشغيل الـ Job.

 

الكروتينات خفيفة جداً

تمتاز الكروتينات بخفة إنشائها, فهي لاتكلف جهد حين إنشائها وإستخدامها. ففي المثال التالي سنرى الفرق الشاسع بين إنشاء مليون ونصف كروتين وخيط حاسوبي والوقت المستغرق لكل من العمليتين.

بالنسبة للكروتينات:

fun main() {
    val result = AtomicInteger()

    for (i: Int in 1..1_500_000) {
        GlobalScope.launch { 
            result.getAndIncrement()
        }
    }

    println(result.get())
}
  • لم يتطلب الامر سوى ثانيتان لإنشاء مليون ونصف كروتين.
  • لاحظ اننا لم نحتاج الى كتابة المرسل Dispatcher للبناء launch لإن بشكل افتراضي سيكون نوعة defualt, هنا فقط اختصرنا الامر.

 

بالنسبة للخيوط الحاسوبية:

fun main() {
    val result = AtomicInteger()

    for (i: Int in 1..1_500_000) {
        thread(start = true) {
            result.getAndIncrement()
        }
    }

    println(result.get())
}
  • تطلب الامر دقيقتان!

 

الكروتينات اخف عند الإنشاء من الخيوط الحاسوبية.

 

هيكلية الكروتينات

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

 

في هذا الدرس رأينا طريقة إنشاء الكروتين Coroutine (وكيف قام بتأجيل نفسه عندما شارف البرنامج على الانتهاء) وذلك بتحديد نطاق له Scope وبإستخدام بناء Builder لبناءة في داخل ذلك النطاق ومن ثم استخدام موزع Dispatcher حتى يأخذه ليضعه على الخيط الحاسوبي. وكذلك تعرفنا على الدوال التأجيلية وماهي الا نوع آخر من طرق إنشاء الكروتين. وكذلك عرفنا بالرغم من أن الكروتين خفيف الا اننا يجب ان نقوم بهيكلته بشكل جيد حتى لاتحدث مشاكل في الذاكرة. في الدروس القادمة سنتطرق الى هذه الاشياء بشكل مفصل.

 

المصادر

المحاضر

Mohammad Laif

محتوى الدورة

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

عن الدرس

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

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

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

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