Kotlin / التزامن والتكامل والتصميم المترابط / الدرس 13
امتلاك العمل المتزامن باستخدام كوروتينات منظمة
ميّز التعليق من الحجب، واستخدم نطاقات الكوروتينات وlaunch وasync وawait وافتراضات الموزّع بصورة مقصودة.
في هذه الصفحة
أهداف الدرس
- اشرح لماذا لا تبدأ suspend العمل بنفسها، ولا تختار خيطا خلفيا تلقائيا.
- أبق الكوروتينات الأبناء ضمن عمر يملكه المستدعي، واجمع نتائجها داخل ذلك العمر.
التعليق قدرة لغوية وليس اختيارا لخيط التنفيذ
تستطيع الدالة المعلقة أن تتوقف ثم تستأنف ضمن آليات الكوروتينات. لا يبدأ المعدّل suspend كوروتينا بنفسه، ولا يجعل الشيفرة متوازية، ولا ينقل العمل الحاجب إلى خيط خلفي. تُنفّذ الكوروتينات على خيوط وفقا لسياقها وموزّعها. وتأتي معظم أدوات البناء عالية المستوى والأدوات المساعدة من kotlinx.coroutines، وهي مكتبة منفصلة عن اللغة والمكتبة القياسية. تتطلب أمثلة الدرس اعتمادا متوافقا سبق إعداده في مشروع المتعلم. لا تحتوي بيئة التأليف على مترجم Kotlin أو اعتماد الكوروتينات، ولذلك تظل جميع البرامج أمثلة توضيحية غير منفذة.
يعطي النطاق للكوروتينات الأبناء عمرا واضح الملكية
تنشئ coroutineScope نطاقا معجميا لا ينتهي حتى تنتهي كتلته والكوروتينات الأبناء المنشأة داخله. إذا تسبب فشل عادي لأحد الأبناء في فشل النطاق، تُلغى الكوروتينات الأشقاء ويُعاد رمي الإخفاق إلى المستدعي وفقا لعقد النطاق. يناسب ذلك مهمة تحتاج إلى جميع أجزائها. تجنب إطلاق العمل في عمر عام غير مرتبط لمجرد العودة فورا، لأن المستدعي يفقد حينها بساطة التحكم في الإلغاء وملكية الإخفاق. وحتى العمل طويل العمر داخل التطبيق يحتاج نطاقا له مالك مقصود ويلغيه عند نهاية دورة حياته، لا نطاقا مجهولا يُنشأ لكل طلب.
استخدم async لنتيجة ستنتظرها المهمة
تعيد async قيمة Deferred، وتسترجع await نتيجتها أو تبلغ عن فشلها. الكتلتان async في المثال كوروتينان ابنان للنطاق نفسه، ولا يُطبع المجموع حتى تتوفر النتيجتان. تحتويان حسابين ثابتين صغيرين، ولذلك يوضح المثال البنية لا تسريعا متوازيا مفيدا. لا ينبغي ترك Deferred حاوية أخطاء مهجورة. في النطاقات المنظمة العادية، قد يلغي فشل كوروتين ابن النطاق حتى قبل الوصول إلى await لاحقة؛ لذلك لا يعد التقاط استثناء await وحده في موضع غير مناسب طريقة عامة لعزل الإخفاقات.
import kotlinx.coroutines.*
suspend fun total(): Int = coroutineScope {
val first = async { 2 }
val second = async { 3 }
first.await() + second.await()
}
suspend fun main() {
println(total())
}
5
المخرج المتوقع 5. يحتاج البرنامج kotlinx.coroutines متوافقة. ولا يدّعي زمنا مقاسا أو عدد خيوط أو تنفيذا بالمترجم أو تحسنا في الأداء.
استخدم launch للإنجاز دون حمولة نتيجة
تعيد launch كائن Job يمثّل دورة الحياة والاكتمال، لا قيمة نتيجة عمل. تنتظر join اكتماله، ولا تحوّله إلى Deferred تحمل نتيجة. يستخدم المثال runBlocking جسرا صغيرا من مدخل متزامن على نمط JVM؛ وهو يحجب خيط المستدعي حتى يكتمل عمل الكوروتينات. ليس ذلك توصية بتغليف معالجات واجهة المستخدم أو كل دالة معلقة باستخدام runBlocking. داخل شيفرة الكوروتينات، استدع الدوال المعلقة مباشرة أو استخدم أداة البناء المنظمة المناسبة، بدلا من إدخال جسور حاجبة بصورة متكررة.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch { println("child complete") }
job.join()
println("joined")
}
child complete
joined
السطران المتوقعان هما child complete ثم joined، لأن join لا تكتمل حتى ينتهي الكوروتين الابن. يحتاج البرنامج kotlinx.coroutines، ولم يُترجم أو يُنفّذ هنا.
اختر الموزّع وفقا للعمل والمنصة الفعليين
يضع حساب كثيف المعالج واستدعاء مكتبة حاجبة متطلبات مختلفة على الخيوط. يجب أن يناسب اختيار الموزّع المنصة وسلوك المكتبة ودورة حياة التطبيق؛ وتحتاج Dispatchers.Main أيضا تكاملا مناسبا مع المنصة. لا يجعل تبديل السياق كائنا مشتركا غير آمن آمنا بين الخيوط، ولا تنقل coroutineScope العمل وحدها إلى موزّع خلفي. فضّل مكتبات ذات واجهات تعليق حقيقية حيث تتوفر. وثّق العمليات الحاجبة، وكيف تُلاحَظ إشارة الإلغاء، والمالك الذي يحد التوازي، قبل ادعاء بقاء البنية مستجيبة تحت الحمل.
الأخطاء الشائعة وإصلاحها
تجمّد دالة suspend خيط واجهة المستخدم الذي يستدعيها.
- لماذا يحدث؟
- بقي العمل الحاجب في سياق المستدعي، ولم تغيّر suspend مكان تنفيذه تلقائيا.
- كيف تصلحه؟
- استخدم واجهة تعليق مناسبة، أو حدا صريح الملكية لتنفيذ العمل الحاجب.
- كيف تتجنب تكراره؟
- وثّق السلوك الحاجب بدلا من استنتاج غيابه من كلمة suspend.
يستمر العمل بعد انتهاء الشاشة أو الطلب المالك له.
- لماذا يحدث؟
- أُطلق الكوروتين في نطاق ذي عمر غير مرتبط بالعمر المطلوب.
- كيف تصلحه؟
- اربط العمل بنطاق تملكه دورة الحياة المعنية وتلغيه عند انتهائها.
- كيف تتجنب تكراره؟
- راجع المالك الفعلي لكل نطاق كوروتينات يُنشأ.
يُعامل Job كما لو كان يحمل نتيجة async.
- لماذا يحدث؟
- اختلطت أدوات البناء التي تمثل الاكتمال بتلك التي تنتج قيمة.
- كيف تصلحه؟
- استخدم async وawait للحصول على قيمة، وlaunch وjoin لانتظار الاكتمال.
- كيف تتجنب تكراره؟
- اقرأ نوع الرد لأداة البناء قبل تركيب المهام المتزامنة.
اختبر فهمك
1. هل ينشئ استدعاء دالتين suspend الواحدة بعد الأخرى تزامنا بينهما؟
اعرض الحل المشروح
لا. تحافظ الاستدعاءات المتتابعة على ترتيبها المتتابع، ما لم يبدأ تنفيذها عملا متزامنا داخليا. عندما يجوز تداخل العمليتين، استخدم نطاق كوروتينات منظما وأنشئ داخله كوروتينات أبناء باستخدام أدوات بناء تختارها عمدا، ثم انتظر نتائجها. تحقّق أولا من أن التنفيذ المتزامن يحفظ ترتيب قواعد العمل وحدود الموارد؛ فالتزامن اختيار له كلفته الدلالية، وليس نتيجة تلقائية لوجود suspend.
2. لماذا لا يناسب runBlocking إصلاحا عاما داخل دالة معلقة؟
اعرض الحل المشروح
يدخل runBlocking حجبا إلى شيفرة تملك أصلا سياق كوروتينات، وقد يهدر موارد تنفيذ محدودة أو يسبب تعارضا يمنع تقدمها، حسب التصميم المحيط. استدع العمليات المعلقة مباشرة، واستخدم coroutineScope أو withContext للبنية أو السياق المطلوبين. الجسر عند نقطة دخول متزامنة حد محدد، وليس غلافا عاما لكل شيفرة الكوروتينات.
أهم ما تعلمته
- تعطي البنية المنظمة للعمل المتزامن مالكا للعمر وللإخفاق.
- التعليق والحجب والتزامن والتوازي مفاهيم مختلفة يجب التمييز بينها.