runBlocking — منشئ Coroutine في Kotlin يحجر الخيط الحالي حتى اكتمال الكوروتين الممرر. بالخلاف من launch و async، ليس دالة suspend ويمكن استدعاؤه من كود عادي (حاجز). وفقًا لـ وثائق JetBrains، 2024، يعمل runBlocking كجسر بين العالم المتزامن وغير المتزامن، مما يسمح بتشغيل الكوروتينات من دالة main والاختبارات.
النقاط الرئيسية
runBlocking هي دالة Kotlin تنشئ CoroutineScope جديداً وتشغل الكوروتين الممرر، مما يحجر الخيط الحالي حتى اكتماله بالكامل. بالخلاف من جميع منشئات Coroutine الأخرى، runBlocking ليست دالة suspend ويمكن استدعاؤها من كود متزامن عادي. تأخذ دالة runBlocking CoroutineContext وكتلة suspend، وتعيد نتيجة من نوع T.
public fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
runBlocking تشغل event-loop جديداً على الخيط الحالي. عندما تستدعي الكوروتين دالة suspend (مثل delay() أو await())، runBlocking تحجر الخيط وتنفذ كوروتينات أخرى مجدولة على نفس الخيط حتى تستؤنف المعلقة. هذا هو الحجز التعاوني — الخيط ليس خاملاً بل يعالج كوروتينات أخرى.
تعتمد آلية runBlocking الداخلية على event-loop: عند استدعاء دالة suspend، توقف runBlocking تنفيذ الكتلة الحالية وتشغل كوروتينات أخرى من الطابور. عندما تكتمل دالة suspend، يستؤنف التنفيذ. يستمر هذا الدورة حتى تنتهي جميع الكوروتينات.
runBlocking تستخدم مجموعة خيوط أحادية خاصة بها لتنفيذ الكوروتينات. بخلاف Dispatchers.IO أو Default، runBlocking لا تبدل الخيوط — بل تعالج جميع الكوروتينات على الخيط الحالي، متداولة تنفيذها. هذا هو المنشئ الوحيد الذي يضمن التنفيذ على نفس الخيط.
fun main() {
val threadName = Thread.currentThread().getName()
println("قبل runBlocking على $threadName")
val result = runBlocking {
println("داخل runBlocking على ${Thread.currentThread().getName()}")
delay(500L)
"Done"
}
println("بعد runBlocking: $result")
}
سيظهر الإخراج أن جميع الأوامر println الثلاثة تتنفذ على نفس الخيط. runBlocking لا تبدل الخيوط بل تنظم تعدد مهام تعاونياً داخل خيط واحد باستخدام event-loop.
runBlocking مبررة في ثلاثة سيناريوهات: نقطة الدخول main() في تطبيقات واجهة الأوامر، اختبارات الوحدة لدوال suspend والجسر — استدعاء كود suspend من مكتبات قائمة على callbacks أو حاجزة. في كود Android الإنتاجي، استخدامه على الخيط الرئيسي ممنوع بشكل قاطع.
| السيناريو | الإمكانية | المخاطر |
|---|---|---|
| main() لتطبيق واجهة الأوامر | نعم | لا يوجد — هي نقطة الدخول، الخيط لا يحجر واجهة المستخدم |
| اختبارات JUnit | نعم | أدنى — الاختبارات متزامنة بالتعريف |
| خيط واجهة المستخدم في Android | لا | ANR، تأخر، تجميد الواجهة |
| Callback → Coroutine | نعم، بحذر | حجز مجموعة الخيوط في العمليات الطويلة |
لاختبارات Android، استخدم kotlinx-coroutines-test مع TestDispatcher بدلاً من runBlocking. يوفر ذلك التحكم في الوقت، التنظيف التلقائي وعزل الاختبارات.
في معظم السيناريوهات، يمكن وينبغي استبدال runBlocking ببدائل غير متزامنة. لـ Android، هذه هي viewModelScope، lifecycleScope أو CoroutineScope مع الموزع المناسب. للاختبارات — TestCoroutineDispatcher و runTest.
// سيئ: runBlocking على الخيط الرئيسي لـ Android
runBlocking(Dispatchers.Main) {
val result = networkApi.fetchData()
textView.setText(result)
}
// جيد: lifecycleScope
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { networkApi.fetchData() }
textView.setText(result)
}
للاختبارات، البديل هو runTest من kotlinx-coroutines-test. ينشئ TestCoroutineScope بوقت افتراضي، مما يسمح باختبار التأخيرات دون انتظار حقيقي. هذا يسرع الاختبارات ويجعلها حتمية.
أكثر السيناريوهات شيوعًا هو اختبار دوال suspend. runBlocking في الاختبارات تسمح بالانتظار المتزامن لنتيجة الكوروتين دون تغيير المعمارية. السيناريو الثاني هو مكتبات بواصلات برمجة API قائمة على callbacks، حيث تستدعى دوال suspend من سياق حاجز عبر runBlocking.
// اختبار دالة suspend باستخدام runBlocking
class RepositoryTest {
@Test
fun `fetchUser returns correct data`() {
val repository = UserRepository(FakeApi())
val result = runBlocking {
repository.fetchUser("123")
}
assertEquals("John", result.name)
assertEquals("john@test.com", result.email)
}
}
للجسر بين callbacks وعالم suspend، استخدم CompletableDeferred مع runBlocking بدلاً من callbacks — هذا يبسط سلاسل العمليات غير المتزامنة ويحسن قراءة الكود.
الاستخدام غير الصحيح لـ runBlocking هو أحد الأخطاء الشائعة عند الانتقال من نهج الحجز إلى الكوروتينات. المشاكل الرئيسية: الاستدعاء على الخيط الرئيسي لـ Android، تداخل runBlocking داخل بعضها البعض، استخدامها داخل دوال غير متزامنة وتشغيل عمليات طويلة عبر runBlocking.
القاعدة الذهبية: runBlocking هي جسر، ليس بديلاً. استخدمها فقط لربط العوالم الحاجزة وغير الحاجزة. لجميع المهام الأخرى، استخدم launch، async أو lifecycleScope.
الأسئلة الشائعة
runBlocking هي المنشئ الوحيد الذي ليس دالة suspend. تبدأ event-loop على الخيط الحالي ولا تعيد التحكم حتى تكتمل جميع الكوروتينات. launch و async تعيدان التحكم فوراً، منفذتين الكوروتين في الخلفية.
غير موصى به. ViewModel لديها viewModelScope مضمن يدير الكوروتينات تلقائياً ويلغيها عند الإتلاف. runBlocking في ViewModel تحجر الخيط ولا تستجيب لإلغاء دورة الحياة.
استخدم runTest من مكتبة kotlinx-coroutines-test. يوفر TestCoroutineScope مع التحكم في الوقت الافتراضي، الإلغاء التلقائي والتنفيذ الحتمي.
Event-loop هو دورة معالجة الأحداث داخل runBlocking. عندما تتعلق كوروتين (مثل delay())، يتحول event-loop إلى تنفيذ كوروتينات أخرى جاهزة على نفس الخيط. هذا يخلق وهم تعدد المهام دون تبديل الخيوط.
runBlocking المتداخلة على نفس الخيط تنشئ إغلاقاً متبادلاً — الكتلة الخارجية تنتظر الداخلية، ولكن الداخلية لا يمكنها البدء حتى تنتهي الخارجية. على خيوط مختلفة يسمح بها ولكن غير موصى به بشدة بسبب تعقيد التصحيح.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا