الروتينات المساعدة (Coroutines) في لغة Kotlin
ملاحظة: ما تزال الروتينات المساعدة تجريبيةً في الإصدار Kotlin 1.1.
الروتينات المساعدة (Coroutines)
تُنشِئ بعض الواجهات API عملياتٍ طويلة التنفيذ (مثل عمليات الدخل والخرج للشبكة أو الدخل والخرج للملفات أو الأعمال المُعقَّدة في الوحدتين CPU أو GPU)، وهذا سيجبر المُستدعي على تجميد تنفيذه (block) إلى حين إتمامها، ولذا توفِّر الروتينات المساعدة طريقةً لمنع تجميد الخيوط (threads) وإجراء عمليةٍ أقلَّ كلفةً (بالنسبة لزمن المعالجة) وأكثر قابليةً للتحكم، ألا وهي الإيقاف المؤقت (suspension) للروتينات المساعدة.
تبسّط الروتينات المساعدة البرمجة غير المتزامنة (asynchronous programming) من خلال وضع التعقيدات البرمجية في مكتباتٍ، ويُعبَّر عن منطق البرنامج تسلسليًا (دون أخذ تعقيدات التنفيذ المتزامن في الحسبان) في الروتين المساعد وستقوم المكتبة بمهمة التنفيذ غير المتزامن، إذ تُغلِّف الأجزاء المرتبطة من شيفرة المستخدم مع دوال ردّ النداء (callbacks) وتصادق على أيّ أحداثٍ متعلِّقة، كما وتجدول التنفيذ ما بين الخيوط (threads) المختلفة (أو ما بين آلاتٍ مختلفة)، وتبقى الشيفرة بالنهاية بسيطةً كما لو أنها مُنفَّذة تسلسليًا.
وقد يُعرَّف استخدام عددٍ من الآليات غير التزامنيّة (asynchronous mechanisms) المتاحة في لغات البرمجة الأخرى كمكتباتٍ بالاعتماد على الروتينات المساعدة في Kotlin، مثل async/await
في لغتيّ C# و ECMAScript ، أو channels و select
في لغة Go، وgenerators/yield
في لغتيّ C# و Python.
التجميد (Blocking) والإيقاف المؤقت (Suspending)
إن الروتينات المساعدة هي بالأساس عملياتٌ قابلة للإيقاف المؤقت بدون تجميد الخيط (thread)، لأنّ تجميد الخيوط مكلفٌ وخاصةً عندما يكون هناك الكثير من التعقيد، إذ تجب المحافظة على عددٍ قليلٍ نسبي من الخيوط، وسيسبب تجميد إحداها تأخير بعض العمليات المهمّة، ومن جانبٍ آخر تكون الروتينات المساعدة بلا كلفةٍ نسبيًا، إذ لا تتطلَّب أي تغيير في السياق (context) أو تدخّل نظام التشغيل، والأهم من كل ذلك هو أنّ مكتبة المستخدم تستطيع التحكم بالإيقاف المؤقت لحدٍ كبيرٍ؛ إذ يكون بالإمكان تحديد ما سيحدث عقب الإيقاف المؤقت والتصرُّف حسب المتطلبات، أمّا الفرق الآخر بين التجميد والإيقاف المؤقت هو أنّ الإيقاف المؤقت للروتينات المساعدة غير مسموحٍ بأي تعليمة عشوائيًا وإنما -فقط- عند نقاطٍ مُحدَّدةٍ تدعى "نقاط الإيقاف المؤقت" (suspension points)، والتي هي في الواقع استدعاءاتٌ لدوالٍ مُحدَّدةٍ خاصة.
دوال الإيقاف المؤقت (Suspending Functions)
يحدث الإيقاف المؤقت عبر استدعاء دوالٍ مُحدَّدةٍ بالكلمة المفتاحية suspend
كما في الشيفرة:
suspend fun doSomething(foo: Foo): Bar {
...
}
وتسمى هذه الدوال بدوال الإيقاف المؤقت لأنّ استدعاءها سيؤدي لإيقاف الروتينات المساعدة مؤقتًا، (وقد تقرر المكتبة الاستمرار بدون الإيقاف المؤقت إن كانت نتيجة الاستدعاء متاحةً بالأصل).
تقبل دوال الإيقاف المؤقت عددًا من المتحولات وقد تعيد قيمًا كما هو الحال في الدوال الأخرى، ولكن لا يمكن استدعاؤها إلا من الروتينات المساعدة أو دالة إيقافٍ مؤقتٍ أخرى أو قيمةٍ مباشرةٍ للدالة (inlined) ضمن أيِّ مما سبق.
للبدء بأيّ روتينٍ مساعدٍ يجب توافر دالة إيقافٍ مؤقتٍ واحدةٍ على الأقل (وغالبًا ما تكون lambda)، لنأخذ مثلًا الدالتين async/await
، فإن كانت الدالة المُبسّطة async()
(من المكتبة kotlinx.coroutines
) بالشكل الآتي:
fun <T> async(block: suspend () -> T)
وهي دالة نظاميّة (regular) وليست دالة إيقافٍ مؤقتٍ، أما المتحوّل الوسيط block
فله نوع الدالة المُحدَّدة بالكلمة المفتاحيّة suspend
(أي suspend () -> T
)، وبالتالي فإنّه عند تمرير lambda إلى الدالةasync()
فإنها ستكون lambda الإيقاف المؤقت (suspending lambda) ويمكن عندئذٍ استدعاء دالة الإيقاف المؤقت منها بالشكل:
async {
doSomething(foo)
...
}
ملاحظة: لا يُسمَح باستخدام دوال التأجيل كأصنافٍ عليا (supertypes) ولا يتوفَّر الدعم كذلك لدوال الإيقاف المؤقت المجهولة (anonymous suspending functions).
أمّا الدالة await()
فقد تكون دالة إيقافٍ مؤقتٍ (ويمكن بالتالي استدعاؤها من داخل الدالة async
) والتي ستوقف الروتين المساعد مؤقتًا إلى حين القيام ببعض العمليات وإعادة نتيجتها، أي:
async {
...
val result = computation.await()
...
}
المزيد عن آلية عمل للدالتين async/await
في kotlinx.coroutines
ويُلاحَظ أنّه من غير الممكن استدعاء دالتي الإيقاف المؤقت await()
و doSomething()
من القيم الحرفيّة للدوال (function literals) غير المباشرة داخل بنية دالة الإيقاف المؤقت ولا من الدالة النظامية (مثل main()
) كما في الشيفرة:
fun main(args: Array<String>) {
doSomething() // من الخطأ استدعاء دوال التأخير من السياق خارج الروتينات المساعدة
async {
...
computations.forEach { // دالة مباشرى
// أيضًا مباشرة lambda
it.await() // شيفرة صحيحة
}
thread { // ليست دالة مباشرة
// غير مباشرة أيضًا lambda
doSomething() // خطأ
}
}
}
وقد تكون دوال الإيقاف المؤقت وهميّةً (virtual) ويجب عند إعادة تعريفها وجود المُحدِّد suspend
كما في الشيفرة:
interface Base {
suspend fun foo()
}
class Derived: Base {
override suspend fun foo() { ... }
}
التوصيف @RestrictsSuspension
قد تُحدَّد الدوال الإضافيّة (extension functions) وتعابير lambdas بالمٌحدِّد suspend
كما في الدوال النظاميّة (regular)، وهذا يسمح بإنشاء لغاتٍ مُخصَّصة المجال (DSL) وواجهات API قابلةٍ للتوسيع (extend) من المستخدِم، وقد يحتاج -ببعض الحالات- مُنشِئ المكتبة إلى منع المستخدِم من إضافة أيّة طريقة للإيقاف المؤقت للروتين المساعد، وذلك باستخدام التوصيف @RestrictsSuspension
للصنف المُستقبِل (receiver class) أو الواجهة R
وحينها يجب تعميم (delegate) كل إضافات الإيقاف المؤقت إلى عناصر في الصنف R
أو إلى إضافاتٍ له، ولأنه من غير الممكن تعميم الإضافات لبعضها الآخر بشكلٍ غير منتهٍ (ولن ينتهي البرنامج عندئذٍ) فهذا سيضمن حدوث كلّ عمليات الإيقاف المؤقت أثناء استدعاء عناصر R
التي يتحكّم بها مُنشِئ المكتبة بشكل تامّ.
وهذا جيّد في حالاتٍ نادرةٍ عندما يُعالج كل إيقافٍ مؤقتٍ بطريقةٍ مختلفةٍ في المكتبة، فعند تعريف استخدام (implement) المُولِّدات مثلًا باستخدام الدالة buildSequence()
فيجب التأكد من أن أيّ استدعاءٍ للإيقاف المؤقت في الروتين المساعد سينتهي باستدعاء الدالة yield()
أو yieldAll()
وليس أيّ دالة أخرى، ولهذا يُوصَف الصنف SequenceBuilder
بالتوصيف @RestrictsSuspension
كما يلي:
@RestrictsSuspension
public abstract class SequenceBuilder<in T> {
...
}