چرا مدیریت همزمانی سخت است؟

هر برنامه‌نویسی که با Thread کار کرده باشد، طعم تلخ race condition، deadlock و memory leak ناشی از Threadهای رها‌شده را چشیده است. مشکل اصلی همزمانی سنتی این است که عمر یک عملیات موازی هیچ ارتباط ساختاریافته‌ای با کد فراخواننده‌ی آن ندارد. وقتی یک Thread جدید می‌سازید، آن Thread زندگی مستقل خودش را شروع می‌کند؛ اگر تابع فراخواننده throw کند، return کند یا حتی از بین برود، آن Thread همچنان در پس‌زمینه به کارش ادامه می‌دهد، مگر اینکه شما صریحاً آن را cancel یا join کنید.
این «بی‌ساختاری» باعث می‌شود:

  • ردیابی اینکه چه Threadهایی هنوز فعال هستند، عملاً غیرممکن شود.
  • خطای رخ‌داده در یک Thread فرزند، به‌سادگی گم شود یا کل برنامه را crash کند.
  • لغو (cancellation) یک عملیات، به‌صورت دستی و مستعد خطا پیاده‌سازی شود.
  • تست‌نویسی برای کد همزمان، به کابوس تبدیل شود.

Kotlin Coroutines با معرفی مدل Structured Concurrency دقیقاً همین مشکل را حل می‌کند: هر عملیات موازی، در یک دامنه (Scope) مشخص متولد می‌شود و عمر آن به عمر آن Scope گره می‌خورد.

 Coroutine دقیقاً چیست؟

Coroutine یک واحد محاسباتی قابل تعلیق (suspendable) است؛ یعنی می‌تواند در یک نقطه از اجرا متوقف شود، کنترل را به caller برگرداند، و بعداً از همان نقطه ادامه پیدا کند — بدون اینکه Thread زیرین را مسدود (block) کند.

از نظر فنی، Coroutine یک light-weight thread نیست به معنای واقعی کلمه؛ بلکه یک state machine است که کامپایلر Kotlin آن را از تابعی با کلیدواژه‌ی suspend می‌سازد. وقتی به یک نقطه‌ی تعلیق (suspension point) مثل delay() یا یک فراخوانی شبکه می‌رسید، Coroutine حالت خودش local variables) ، program counter و(...  را ذخیره می‌کند، Thread را آزاد می‌گذارد تا کارهای دیگر انجام دهد، و بعداً — احتمالاً روی یک Thread دیگر — ادامه پیدا می‌کند.

suspend fun fetchUser(id: Int): User {
    delay(1000) // Thread را block نمی‌کند، فقط Coroutine را معلق می‌کند
    return userApi.getUser(id)
}

تفاوت Coroutine و Thread

ویژگیThreadCoroutine
مدیریت‌شده توسطسیستم‌عامل (OS)Kotlin Runtime
هزینه‌ی ساختنسبتاً سنگین(چند مگابایت Stack)بسیار سبک (چند صد بایت)
تعداد قابل اجرا هم‌زمانمحدود (هزاران)میلیون‌ها
نوع مسدودسازیBlockingNon-blocking (Suspending)
Context Switchتوسط OS ، پرهزینهتوسط Kotlin Runtime ، ارزان
قابلیت لغو ساختاریافتهندارد به‌صورت پیش‌فرضبله Structured) (Concurrency

 

نکته‌ی کلیدی این است که چند Coroutine می‌توانند روی یک Thread واحد اجرا شوند و به نوبت آن را در اختیار بگیرند، بدون اینکه نیاز به Context Switch سنگین سطح OS باشد. این دقیقاً همان چیزی است که اجرای هزاران عملیات هم‌زمان (مثلاً هزاران اتصال شبکه) را در Coroutine عملی می‌کند، در حالی‌که با Thread واقعی غیرممکن یا بسیار پرهزینه است.

مفهوم  Structured Concurrency

 Structured Concurrency یک قانون ساده اما قدرتمند دارد:
هیچ Coroutine ای نباید بدون یک «والد» مشخص اجرا شود، و یک Coroutine والد تا زمانی که همه‌ی فرزندانش تمام نشده‌اند، به پایان نمی‌رسد.
این یعنی هر Coroutine در یک  CoroutineScope ساخته می‌شود، و آن Scope مسئولیت مدیریت چرخه‌ی حیات همه‌ی Coroutineهای داخل خودش را بر عهده دارد. نتیجه‌ی این طراحی:

  1. هیچ عملیاتی گم نمی‌شود: اگر Scope والد لغو شود، تمام فرزندان هم لغو می‌شوند.
  2. خطاها به‌صورت خودکار propagate می‌شوند: یک خطا در فرزند، والد را هم مطلع می‌کند مگر با (SupervisorJob).
  3. کد به‌صورت خطی و قابل‌فهم باقی می‌ماند: حتی وقتی چندین عملیات به‌صورت موازی اجرا می‌شوند، ساختار کد سلسله‌مراتبی و قابل پیش‌بینی است.

مقایسه کنید:

// روش قدیمی و بدون ساختار (callback / thread خام)
fun loadData() {
    thread {
        val result = heavyComputation()
        updateUi(result) // اگر Activity از بین رفته باشد چه؟
    }
}

// Structured Concurrency
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val result = heavyComputation()
            updateUi(result) // اگر ViewModel از بین برود، این Coroutine خودکار لغو می‌شود
        }
    }
}

CoroutineScope  و Job

CoroutineScope  یک رابط ساده است که فقط یک  CoroutineContext نگه می‌دارد و مسئول تعیین «مرز» زندگی Coroutineهاست. هر تابعی که Coroutine جدید بسازد (launch,asyncs) باید در بستر یک Scope اجرا شود.

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Job  قلب Structured Concurrency است Job .نماینده‌ی یک کار قابل لغو است و یک ساختار درختی می‌سازد: هر Job می‌تواند فرزندانی داشته باشد، و وضعیت آن، Completing، Completed، Cancelling، بر اساس وضعیت فرزندانش تعیین می‌شود.

val job = CoroutineScope(Dispatchers.Default).launch {
    launch { /* فرزند ۱ */ }
    launch { /* فرزند ۲ */ }
}
job.cancel() // هر دو فرزند هم لغو می‌شوند

نکته‌ی مهم: هرگز نباید یک  GlobalScope بی‌رویه بسازید، چون این کار عملاً Structured Concurrency را دور می‌زند و شما را به همان مشکلات Thread خام برمی‌گرداند. به‌جای آن از Scopeهای مدیریت‌شده مثل viewModelScope، lifecycleScope یا Scope سفارشی با Job مشخص استفاده کنید.

class Repository {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun close() {
        scope.cancel() // تمام کارهای در حال اجرا را تمیز پاک می‌کند
    }
}

Launch  در برابر async

هر دو، Coroutine Builder هستند، اما هدف‌شان متفاوت است:

 launch یک Coroutine آتش‌وفراموش (fire-and-forget) می‌سازد و Job برمی‌گرداند. برای عملیاتی که (نتیجه‌ی مستقیمی نمی‌خواهید)مثل لاگ کردن یا آپدیت : 

scope.launch {
    saveToDatabase(user)
}

 async یک Coroutine می‌سازد که مقداری را به‌صورت غیرهم‌زمان محاسبه می‌کند و  Deferred<T> برمی‌گرداند؛ برای گرفتن نتیجه باید   await()  را صدا بزنید:

val deferredUser = scope.async { fetchUser(id) }
val deferredPosts = scope.async { fetchPosts(id) }

val user = deferredUser.await()
val posts = deferredPosts.await()

یک تفاوت حیاتی در مدیریت خطا وجود دارد: در launch، خطای رخ‌داده بلافاصله propagate می‌شود. اما در async، خطا داخل Deferred «نگه‌داشته» می‌شود و فقط زمانی که  await() صدا زده شود، دوباره throw می‌شود. اگر هرگز  await() را صدا نزنید، ممکن است خطا بی‌سروصدا گم شود (مگر در حالت  SupervisorJob که رفتار کمی متفاوت است).

مدیریت خطا و Cancellation

Cancellation  کوآپریتیو (Cooperative)

لغو در Coroutine اجباری (preemptive) نیست؛ بلکه کوآپریتیو است. یعنی Coroutine باید خودش با چک کردن وضعیت  isActive یا فراخوانی توابع suspend استاندارد(که به‌طور خودکار cancellation را چک می‌کنند)، به درخواست لغو پاسخ دهد.

val job = scope.launch {
    var i = 0
    while (isActive) { // بدون این چک، لغو نادیده گرفته می‌شود
        println("کار ${i++}")
        delay(500)
    }
}
delay(2000)
job.cancel()

اگر داخل حلقه‌ای CPU-bound هستید و از تابع suspend استفاده نمی‌کنید، حتماً باید  ensureActive() یا  yield() را به‌صورت دستی صدا بزنید، وگرنه Coroutine هرگز لغو نمی‌شود.
 

پاک‌سازی منابع با finally

scope.launch {
    try {
        downloadFile()
    } finally {
        // این بخش حتی در صورت لغو هم اجرا می‌شود
        closeConnection()
    }
}


نکته‌ی مهم: اگر داخل بلاک finally نیاز به فراخوانی یک تابع suspend دیگر دارید)مثلاً برای پاک‌سازی (async ، باید از  withContext(NonCancellable)  استفاده کنید، چون در لحظه‌ی لغو، Coroutine خودش دیگر اجازه‌ی اجرای عملیات suspend معمولی را ندارد:

finally {
    withContext(NonCancellable) {
        cleanupRemoteSession()
    }
}

 

propagation خطا در سلسله‌مراتب

در حالت پیش‌فرض (Job معمولی)، اگر یک فرزند با خطا شکست بخورد:

  1. آن فرزند لغو می‌شود.
  2. خطا به والد propagate می‌شود.
  3. والد تمام فرزندان دیگرش را لغو می‌کند.
  4. خود والد هم fail می‌شود.

این رفتار «all-or-nothing» برای عملیاتی که به هم وابسته‌اند منطقی است، اما گاهی نمی‌خواهیم شکست یک عملیات، بقیه را هم نابود کند — اینجاست که  SupervisorJob وارد می‌شود.

SupervisorJobو SupervisorScope

 SupervisorJob این رفتار پیش‌فرض propagation خطا را می‌شکند: شکست یک فرزند، فرزندان دیگر یا خود والد را لغو نمی‌کند.

val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + Dispatchers.Default)

scope.launch {
    throw RuntimeException("خطای فرزند اول")
}

scope.launch {
    delay(1000)
    println("این هنوز اجرا می‌شود") // با Job معمولی، این هرگز چاپ نمی‌شد
}

 

نکته‌ی بسیار مهم و منبع اشتباه رایج :SupervisorJob فقط زمانی مؤثر است که مستقیماً به‌عنوان والد یک Scope استفاده شود، نه به‌عنوان پارامتر یک launch تو در تو. یعنی این کد رفتار مورد انتظار را نمی‌دهد:

// اشتباه رایج!
scope.launch(SupervisorJob()) {
    launch { throw Exception("Boom") } // این هنوز کل launch بیرونی را می‌ترکاند
}

 

برای همین بهتر است از تابع  supervisorScope استفاده کنید که به‌طور صحیح این رفتار را در یک بلوک محدود پیاده‌سازی می‌کند:

suspend fun loadDashboard() = supervisorScope {
    val user = async { fetchUser() }
    val notifications = async {
        try {
            fetchNotifications()
        } catch (e: Exception) {
            emptyList() // شکست notifications، user را تحت تأثیر قرار نمی‌دهد
        }
    }
    Dashboard(user.await(), notifications.await())
}

 

برای مدیریت دقیق‌تر خطاهای غیرمنتظره در سطح Scope ، از  CoroutineExceptionHandler  استفاده می‌کنیم (این فقط روی launch، نه async، و فقط روی ریشه‌ی سلسله‌مراتب مؤثر است):

val handler = CoroutineExceptionHandler { _, exception ->
    Log.e("App", "خطای مدیریت‌نشده: ${exception.message}")
}

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)

Dispatchers.IO، Default و Main

Dispatcher تعیین می‌کند که یک Coroutine روی کدام Thread یا Thread Pool اجرا شود:

  •  Dispatchers.Main: برای عملیات مربوط بهUI فقط در محیط‌هایی مثل Android یا JavaFX معنا دارد.این Dispatcher تک‌رشته‌ای است.
  •  Dispatchers.Default: برای عملیات CPU-bound سنگین مثل پردازش لیست‌های بزرگ، الگوریتم‌های محاسباتی، پارس کردن  JSON  بزرگ. تعداد  Threadهای آن معمولاً برابر تعداد هسته‌های CPU است.
  •  Dispatchers.IO: برای عملیات I/O-bound مثل فراخوانی شبکه، خواندن/نوشتن فایل، کوئری دیتابیس. این Dispatcher Thread Pool بزرگ‌تری دارد پیش‌فرض تا ۶۴ Thread یا بیشتر چون فرض بر این است که Threadها بیشتر منتظر (blocked) هستند تا مشغول محاسبه.
suspend fun processImage(bytes: ByteArray): Bitmap = withContext(Dispatchers.Default) {
    // پردازش سنگین CPU
    decodeAndFilter(bytes)
}

suspend fun uploadImage(bitmap: Bitmap) = withContext(Dispatchers.IO) {
    api.upload(bitmap)
}

نکته‌ی مهم: Dispatchers.IO و  Dispatchers.Default روی یک Thread Pool مشترک ForkJoinPool) مبتنی) بنا شده‌اند اما با محدودیت‌های جداگانه، پس جابه‌جایی بین آن‌ها با  withContext نسبتاً ارزان است.

یک مثال واقعی: اجرای چند عملیات همزمان

فرض کنید یک صفحه‌ی داشبورد داریم که باید هم‌زمان اطلاعات کاربر، لیست سفارش‌ها و آمار فروش را از سه سرویس مختلف بگیرد:

data class DashboardData(
    val user: User,
    val orders: List<Order>,
    val stats: SalesStats
)

class DashboardRepository(
    private val userApi: UserApi,
    private val orderApi: OrderApi,
    private val statsApi: StatsApi
) {
    suspend fun loadDashboard(userId: String): Result<DashboardData> = supervisorScope {
        try {
            val userDeferred = async(Dispatchers.IO) { userApi.getUser(userId) }
            val ordersDeferred = async(Dispatchers.IO) { orderApi.getOrders(userId) }
            val statsDeferred = async(Dispatchers.IO) { statsApi.getStats(userId) }

            // اگر کاربر پیدا نشود، کل عملیات بی‌معناست، پس اجازه می‌دهیم exception طبیعی propagate شود
            val user = userDeferred.await()

            // برای دو مورد دیگر، fallback مناسب در نظر می‌گیریم
            val orders = runCatching { ordersDeferred.await() }.getOrDefault(emptyList())
            val stats = runCatching { statsDeferred.await() }.getOrDefault(SalesStats.empty())

            Result.success(DashboardData(user, orders, stats))
        } catch (e: CancellationException) {
            throw e // هرگز CancellationException را قورت ندهید
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

در این مثال:

  • هر سه فراخوانی شبکه‌ای موازی اجرا می‌شوند، نه به‌ترتیب (Sequential).
  • با supervisorScope، شکست یکی، بقیه را قطع نمی‌کند.
  • زمان کل اجرا برابر با کندترین سرویس است، نه مجموع زمان هر سه.
  • کد همچنان خطی و قابل‌خواندن باقی مانده، بدون callback تودرتو.

فراخوانی از ViewModel:

class DashboardViewModel(private val repository: DashboardRepository) : ViewModel() {
    private val _state = MutableStateFlow<DashboardData?>(null)
    val state = _state.asStateFlow()

    fun load(userId: String) {
        viewModelScope.launch {
            repository.loadDashboard(userId)
                .onSuccess { _state.value = it }
                .onFailure { /* نمایش خطا به کاربر */ }
        }
    }
}

وقتی  ViewModel از بین برود،  viewModelScope به‌صورت خودکار cancel می‌شود، و به لطف Structured Concurrency ، تمام async های داخل  supervisorScope هم لغو می‌شوند — بدون نیاز به مدیریت دستی.

تست‌نویسی Coroutine ها با runTest

یکی از مزیت‌های عملی Structured Concurrency این است که تست‌نویسی کد همزمان را قابل‌پیش‌بینی می‌کند. کتابخانه‌ی kotlinx-coroutines-test تابع  runTest را ارائه می‌دهد که یک  TestScope با یک  Virtual Clock می‌سازد؛ یعنی فراخوانی  delay() به‌جای انتظار واقعی، زمان مجازی را جلو می‌برد و تست بلافاصله اجرا می‌شود:

@Test
fun `loadDashboard returns data when all calls succeed`() = runTest {
    val repository = DashboardRepository(fakeUserApi, fakeOrderApi, fakeStatsApi)

    val result = repository.loadDashboard("user-1")

    assertTrue(result.isSuccess)
}

 

برای تعویض Dispatcher واقعی (Dispatchers.IO,Dispatchers.Default)  با یک Dispatcher قابل‌کنترل در تست، الگوی رایج تزریق  CoroutineDispatcher به‌عنوان وابستگی است:

class DashboardRepository(
    private val userApi: UserApi,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
    suspend fun loadUser(id: String) = withContext(ioDispatcher) {
        userApi.getUser(id)
    }
}

// در تست:
val repository = DashboardRepository(fakeUserApi, ioDispatcher = StandardTestDispatcher(testScheduler))

 

این الگو اجازه می‌دهد بدون هیچ  Thread.sleep واقعی و بدون flakiness، سناریوهای پیچیده‌ی موازی (از جمله race condition و ترتیب اجرا)را قطعی و سریع تست کنید.

Coroutine  و Flow: جریان‌های داده‌ی ناهم‌زمان

 Flow مکملی برای Coroutine است که برای مدل‌سازی جریانی از چند مقدار در طول زمان طراحی شده — برخلاف  suspend fun که فقط یک مقدار واحد برمی‌گرداند Flow هم به‌طور کامل تابع اصول Structured Concurrency  است: یک Flow فقط وقتی cold-stream آن collect می‌شود اجرا می‌گردد و به Scope فراخواننده گره می‌خورد.

fun observeOrders(userId: String): Flow<List<Order>> = flow {
    while (true) {
        emit(orderApi.getOrders(userId))
        delay(5000) // هر ۵ ثانیه به‌روزرسانی
    }
}.flowOn(Dispatchers.IO)

// در ViewModel:
viewModelScope.launch {
    observeOrders(userId).collect { orders ->
        _state.update { it.copy(orders = orders) }
    }
}

 

نکته‌ی کلیدی: عملگر flowOn  جهت جریان داده را تغییر نمی‌دهد، فقط  Context  اجرای بالادست (upstream) را عوض می‌کند؛ و درست مثل launch، وقتی viewModelScope  لغو شود، جمع‌آوری (collect) Flow  هم به‌طور خودکار متوقف می‌شود — دوباره یک نمونه‌ی روشن از Structured Concurrency  در عمل.

 اشتباهات رایج در  Coroutine

۱. استفاده‌ی بی‌رویه از GlobalScope

// اشتباه: عمر این Coroutine به هیچ چیز گره نخورده و leak می‌کند
GlobalScope.launch { doWork() }

 

۲. قورت دادن CancellationException

// اشتباه فاحش: این کار cancellation کل سلسله‌مراتب را می‌شکند
try {
    doSuspendWork()
} catch (e: Exception) {
    // CancellationException هم اینجا catch می‌شود و نادیده گرفته می‌شود!
}

همیشه یا  CancellationException را به‌صورت جداگانه catch و   rethrow  کنید، یا از catch (e: SomeSpecificException) استفاده کنید، نه  Exception عمومی.
 

۳. فراموش کردن await() روی async

// خطا در deferredResult هرگز throw نمی‌شود اگر await صدا زده نشود
val deferredResult = scope.async { riskyOperation() }
// ... کد ادامه پیدا می‌کند بدون اینکه بداند خطایی رخ داده

 

۴. اجرای async پشت‌سرهم به‌جای موازی

// اشتباه: این عملاً موازی نیست، چون await بلافاصله بعد از async صدا زده می‌شود
val a = async { fetchA() }.await()
val b = async { fetchB() }.await()

// درست:
val deferredA = async { fetchA() }
val deferredB = async { fetchB() }
val a = deferredA.await()
val b = deferredB.await()

 

۵. مسدود کردن Thread داخل  Coroutine

// اشتباه: Thread.sleep یک تابع suspend نیست و Dispatcher را عملاً block می‌کند
scope.launch(Dispatchers.Default) {
    Thread.sleep(1000) // به‌جای این از delay(1000) استفاده کنید
}

 

۶. ساخت Scope بدون  SupervisorJob در جایی که نباید یک خطا همه‌چیز را بترکاند 

این باعث می‌شود یک خطای کوچک در یک تسک فرعی، کل ماژول یا حتی کل برنامه را از کار بیندازد.
 

۷. عدم توجه به context switch غیرضروری 

فراخوانی مکرر  withContext  بین Dispatcher های مختلف در یک تابع کوچک، هزینه‌ی غیرضروری اضافه می‌کند؛ بهتر است این جابه‌جایی در سطح بالاتر و کمتر تکرارشونده انجام شود.
 

چه زمانی Coroutine انتخاب مناسبی نیست؟

با وجود قدرت Coroutine ، این ابزار برای هر موقعیتی مناسب نیست:

  • عملیات کاملاً CPU-bound و بلاک‌کننده بدون نقطه‌ی تعلیق: اگر کاری کاملاً محاسباتی و پیوسته است )مثلاً یک الگوریتم فشرده‌سازی که هرگز suspend نمی‌شود)، مزیت Coroutine نسبت به یک Thread Pool ساده محدود می‌شود؛ گرچه هنوز می‌توان از Dispatchers.Default بهره برد، اما مزیت اصلی Coroutine )سبک بودن حین انتظار I/O(اینجا کمرنگ‌تر است.
  • کتابخانه‌ها یا فریمورک‌هایی که صرفاً API مبتنی بر Thread  یا  Callback  دارند و پل‌زدن آن‌ها به دنیای Coroutine هزینه‌ی مهندسی بیشتری نسبت به سود آن دارد. در چنین مواردی گاهی استفاده مستقیم از  Executor یا RxJava (اگر پروژه از قبل روی آن بنا شده) منطقی‌تر است.
  • سیستم‌های بسیار حساس به Real-time با نیاز به کنترل دقیق زمان‌بندی سطح پایین، جایی که رفتار Scheduler کوروتین (که غیرقطعی و مبتنی بر Thread Pool است( کنترل کافی در اختیار برنامه‌نویس نمی‌گذارد.
  • پروژه‌های بسیار کوچک و ساده که اصلاً همزمانی واقعی نیاز ندارند. اضافه کردن dependency و پیچیدگی مفهومی Coroutine برای یک اسکریپت خطی ساده، صرفاً overhead غیرضروری است.
  • زمانی که تیم توسعه دانش کافی از مدل Cancellation کوآپریتیو و Structured Concurrency ندارد. استفاده‌ی نادرست از Coroutine (مثل موارد بخش «اشتباهات رایج») می‌تواند باگ‌هایی بسازد که ردیابی‌شان از باگ‌های Thread سنتی هم سخت‌تر است، چون ظاهر کد ساده و امن به نظر می‌رسد در حالی‌که پشت صحنه رفتار نادرستی دارد.

در نهایت،  Coroutine و مدل Structured Concurrency ابزاری فوق‌العاده قدرتمند برای مدیریت I/O همزمان، ترکیب عملیات ناهمزمان و نوشتن کد خوانا و امن هستند — اما مثل هر ابزار دیگری، باید با درک درست از مکانیزم‌های زیرینش Job hierarchy)، Cancellation کوآپریتیو، تفاوت SupervisorJob و Job معمولی) به‌کار گرفته شوند تا واقعاً مزیت‌شان را نشان دهند.

 

پیوست: بنچمارک‌ها و ادعاهای رایج درباره‌ی کارایی

 

در فضای وبلاگ‌ها و مقالات آموزشی، ادعاهای اغراق‌آمیزی مثل  Coroutine  هزار برابر کم‌مصرف‌تر از Thread است یا ۶ برابر سریع‌تر زیاد دیده می‌شود. واقعیت دقیق‌تر و محتاطانه‌تر است:

  • مصرف حافظه:  در آزمایش‌های مستقل روی اپلیکیشن‌های اندرویدی سناریوهای I/O-bound با تعداد زیاد Thread/Coroutine هم‌زمان، نسبتی نزدیک به ۶ به ۱ بین حافظه‌ی مصرفی یک Thread خام و یک Coroutine مشاهده شده است — یعنی Coroutine واقعاً سبک‌تر است، اما نه در حدی که ادعاهای «هزار برابر» نشان می‌دهند.
  • کارهای :CPU-bound وقتی از Dispatchers.Default برای کار محاسباتی سنگین استفاده می‌شود، مزیت حافظه‌ای Coroutine عملاً از بین می‌رود، چون سقف کارایی را همان تعداد هسته‌های فیزیکی CPU تعیین می‌کند — درست مثل یک Thread Pool معمولی.
  • مقایسه با Virtual Threads جاوا :(Project Loom) پژوهش‌های دانشگاهی جدیدتر نشان داده‌اند که در برخی بارهای کاری، Virtual Threadهای جاوا از نظر مصرف حافظه‌ی heap و میانگین تأخیر  (latency)  در بار سنگین، حتی از Coroutine هم بهتر عمل می‌کنند — نکته‌ای که یادآوری می‌کند سبک‌تر بودن از Thread یک قانون مطلق نیست و به‌شدت به نوع بار کاری (Workload) بستگی دارد.
  • نتیجه‌گیری عملی: به‌جای تکیه بر اعداد بازاریابی‌شده‌ی وبلاگ‌ها، برای تصمیم معماری مهم روی پروژه‌ی واقعی، بهتر است بنچمارک اختصاصی روی همان بار کاری (Workload) پروژه اجرا شود.

 


 

References

  • Kotlin Documentation — Coroutines Guide: https://kotlinlang.org/docs/coroutines-guide.html

  • Kotlin Documentation — Cancellation and Timeouts: https://kotlinlang.org/docs/cancellation-and-timeouts.html

  • Kotlin Documentation — Exception Handling (SupervisorJob): https://kotlinlang.org/docs/exception-handling.html

  • Kotlin Documentation — Coroutine Context and Dispatchers: https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html

  • Kotlin Documentation — Testing Coroutines (runTest): https://kotlinlang.org/docs/coroutines-and-channels.html

  • Roman Elizarov — "Structured Concurrency" (طراح اصلی Kotlin Coroutines)، مقاله‌ی رسمی روی Medium: https://elizarov.medium.com/structured-concurrency-722d765aa952

  • techyourchance.com — "Kotlin Coroutines vs Threads Memory Benchmark" (منبع نسبت حافظه‌ی ۶ به ۱): https://www.techyourchance.com/kotlin-coroutines-vs-threads-memory-benchmark/

  • techyourchance.com — "Kotlin Coroutines vs Threads Performance Benchmark": https://www.techyourchance.com/kotlin-coroutines-vs-threads-performance-benchmark/

  • Beronic, D. et al. — "Comparison of Structured Concurrency Constructs in Java and Kotlin – Virtual Threads and Coroutines" (ResearchGate، مقایسه با Project Loom): https://www.researchgate.net/publication/361589720

  • Android Developers — "Improve app performance with Kotlin coroutines": https://developer.android.com/kotlin/coroutines/coroutines-best-practices