Dispatchers في Kotlin Coroutines هي مكونات CoroutineContext التي تحدد الخيوط لتنفيذ coroutines: Main (خيط واجهة المستخدم)، IO (الشبكة والقرص)، Default (المهام كثيفة الاستخدام لوحدة المعالجة المركزية) و Unconfined (الخيط الحالي). يدير كل مُرسِل مجموعة خيوط متخصصة محسَّنة لنوع معين من العمل. وفقًا لدليل JetBrains، 2024، فإن اختيار المُرسِل الصحيح أمر بالغ الأهمية لأداء التطبيق واستقراره.
الخلاصة
Dispatchers هي تطبيقات لواجهة CoroutineDispatcher، وهي عناصر من CoroutineContext. تحدد على أي خيط أو مجموعة خيوط سيتم تنفيذ coroutine. عند إنشاء coroutine عبر launch أو async، يمكن تمرير المُرسِل كأول معامل: launch(Dispatchers.IO) { ... }. إذا لم يتم تحديد مُرسِل، يتم توريثه من CoroutineScope الخارجي.
يوفر Kotlin أربعة مُرسِلات مدمجة: Main و IO و Default و Unconfined. يستخدم كل مُرسِل مجموعة الخيوط الخاصة به المحسَّنة لنوع معين من العمليات. اختيار المُرسِل الصحيح يحدد أداء التطبيق: الاختيار الخاطئ يؤدي إلى تباطؤ واجهة المستخدم أو خمول أنوية وحدة المعالجة المركزية أو استخدام غير فعال للخيوط.
| المُرسِل | مجموعة الخيوط | أقصى عدد من الخيوط | الاستخدام |
|---|---|---|---|
| Dispatchers.Main | واحد (UI) | 1 | تحديثات واجهة المستخدم، LiveData، View |
| Dispatchers.IO | مجموعة IO | 64 (limitedParallelism) | الشبكة، الملفات، قواعد البيانات |
| Dispatchers.Default | مجموعة CPU | N من الأنوية | الفرز، التحليل، الحسابات |
| Dispatchers.Unconfined | الخيط الحالي | غير متاح | العمليات الوسيطة، الاختبارات |
Dispatchers.Main هو المُرسِل الذي ينفذ coroutines على الخيط الرئيسي لنظام Android. وهو مصمم للعمليات المتعلقة بواجهة المستخدم: تحديث TextView، استدعاء notifyDataSetChanged، العمل مع LiveData و StateFlow. في Android، يتم تنفيذ هذا المُرسِل عبر Handler (Looper.getMainLooper()).
// Correct switch to Main for UI updates
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
إذا كان coroutine موجودًا بالفعل على المُرسِل Main، فإن withContext(Dispatchers.Main) الإضافي لا يُنشئ عبئًا — يتحقق المُرسِل من الخيط الحالي ويتخطى التبديل. withContext هو الطريقة المفضلة للتبديل بين المُرسِلات.
Dispatchers.IO هو مُرسِل محسَّن لعمليات الإدخال/الإخراج: طلبات HTTP (Ktor، OkHttp)، قراءة وكتابة الملفات، العمل مع Room أو SQLDelight. يستخدم مجموعة من 64 خيطًا افتراضيًا، قابلة للتوسع تحت الحمل. يمكن لكل طلب إدخال/إخراج جديد إنشاء خيط إضافي حتى الوصول إلى الحد الأقصى.
للتحكم في عدد عمليات الإدخال/الإخراج المتزامنة، استخدم limitedParallelism(). تنشئ هذه الدالة مُرسِلاً جديدًا بحد أقصى لعدد الخيوط المتوازية، مما يمنع استنزاف المجموعة أثناء العمليات الجماعية.
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Load 100 files with limit of 4 concurrent operations
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
استخدم مُرسِل IO لجميع العمليات حيث يقضي coroutine الوقت في الانتظار (I/O-bound). المهام كثيفة الاستخدام لوحدة المعالجة المركزية على مُرسِل IO غير فعالة — فهي تشغل خيوطًا مخصصة للإدخال/الإخراج، مما يقلل من الإنتاجية للنظام.
Dispatchers.Default هو المُرسِل للعمليات الحسابية التي تُحمِّل المعالج: الفرز، التصفية، تحليل JSON (Moshi، Kotlinx Serialization)، معالجة الصور، الحسابات. حجم المجموعة يساوي عدد أنوية المعالج (ولكن لا يقل عن 2). يضمن ذلك أقصى استفادة من وحدة المعالجة المركزية دون تبديل السياق.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
لا تستخدم Dispatchers.Default لعمليات الإدخال/الإخراج — سيؤدي ذلك إلى حظر خيوط مجموعة وحدة المعالجة المركزية التي يمكنها معالجة المهام الحسابية. يسمح فصل IO و Default بـ الاستخدام الأمثل لموارد النظام: خيوط IO تنتظر الإدخال/الإخراج، وخيوط CPU مشغولة باستمرار بالحسابات.
Dispatchers.Unconfined هو مُرسِل خاص لا يربط coroutine بأي مجموعة. يبدأ coroutine التنفيذ في الخيط الذي تم استدعاء launch/async منه، وبعد التعليق يستأنف في الخيط الذي استدعى resume. هذا السلوك مناسب للعمليات الوسيطة التي لا تتطلب سياقًا ثابتًا.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Before delay: ${Thread.currentThread().getName()}")
delay(500L)
println("After delay: ${Thread.currentThread().getName()}")
}
}
في كود الإنتاج، نادرًا ما يُستخدم Dispatchers.Unconfined. حالات الاستخدام الرئيسية: التحويلات الخفيفة قبل تمرير البيانات إلى مُرسِل آخر والاختبارات. لأعباء العمل الإنتاجية، استخدم مُرسِلات صريحة — Unconfined غير متوقع لأن خيط التنفيذ يعتمد على تنفيذ resume.
يعتمد اختيار المُرسِل على نوع المهمة: عمليات واجهة المستخدم → Main، I/O-bound → IO، CPU-bound → Default، وسيطة → توريث من النطاق. بالنسبة لنظام Android، يُوصى بتشغيل coroutine على المُرسِل حيث يتم العمل الرئيسي، والتبديل إلى Main عبر withContext قبل تحديث واجهة المستخدم.
للسيناريوهات المعقدة، ادمج المُرسِلات باستخدام عامل التشغيل +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. يؤدي ذلك إلى إنشاء CoroutineContext مع مُرسِل محدد ومعالجة الأخطاء وتسلسل هرمي معزول لـ Job.
الأسئلة الشائعة
Dispatchers.IO يستخدم مجموعة تصل إلى 64 خيطًا للعمليات المرتبطة بالإدخال/الإخراج (انتظار الإدخال/الإخراج)، بينما Dispatchers.Default يستخدم مجموعة تعتمد على عدد أنوية وحدة المعالجة المركزية للمهام الحسابية. عندما تكون الخيوط شحيحة، يمكن للمجموعتين مشاركة الخيوط مع بعضهما البعض.
نعم، استخدم newSingleThreadContext() لمجموعة خيط واحد أو newFixedThreadPoolContext() لمجموعة ثابتة. للإنتاج، استخدم limitedParallelism() بناءً على المُرسِلات الموجودة — وهذا أكثر كفاءة من إنشاء مجموعات جديدة.
إذا كان Dispatchers.Main غير متاح (على سبيل المثال، في اختبار JUnit أو خدمة خلفية)، يتم طرح IllegalStateException. استخدم TestCoroutineDispatcher للاختبارات و Dispatchers.IO أو Default للخدمات الخلفية.
استخدم Dispatchers.IO.limitedParallelism(N)، حيث N هو أقصى عدد من الخيوط المتوازية. يمنع ذلك استنزاف المجموعة أثناء الطلبات الجماعية ويوفر توازيًا محكومًا.
Dispatchers.Unconfined مناسب للعمليات الوسيطة: تحويلات البيانات الخفيفة قبل تمريرها إلى مُرسِل آخر، سيناريوهات الاختبار. في كود إنتاج Android، لا يُوصى به بسبب خيط التنفيذ غير المحدد بعد التعليق.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا