跪拜 Guibai
← Back to the summary

Wrapping an Already-Async TaskRunner in Dispatchers.IO Doubles Your App's Thread Count

In-Depth Analysis of Coroutines: With Only 8 Async Tasks, Why Did the App's Thread Count Instantly Double?

In Kotlin coroutines, there is a very subtle but real problem:

You just added Dispatchers.IO, and the thread count increased — not by a little, but exponentially.

More critically:

👉 This problem is more severe during the Application startup phase than in a ViewModel.


0. First, Clarify the "Real Business Scenario"

To make the problem closer to real engineering, let's assume a typical App:


App Startup & UI Initialization Behavior

When the entire application starts, it triggers two types of concurrent tasks simultaneously:

① Application Startup Phase (Global Initialization)

App starts → Automatically triggers 4 concurrent initialization tasks

For example:


② UI Page Startup Phase (Business Concurrency)

Entering the home page → ViewModel automatically triggers 4 concurrent requests

For example:


👉 Two phases total: 8 concurrent tasks


We Simulate with a Unified Task Model

To simplify the problem, we define a task executor:

object TaskRunner {

    private val executor = Executors.newFixedThreadPool(10) {
        Thread(it, "TaskRunner-Pool")
    }

    suspend fun test(time: Long): String =
        withContext(executor.asCoroutineDispatcher()) {
            Thread.sleep(time) // Simulate real IO
            "done on ${Thread.currentThread().name}"
        }
}

Key Points

✔ TaskRunner already has its own thread pool ✔ It is already a complete execution model ✔ It already "knows how to run itself"

This simulates time-consuming tasks like network requests, e.g., Retrofit.

1. ViewModel Scenario: UI Concurrent Requests

Home page ViewModel:

viewModelScope.launch {
    launch(Dispatchers.IO) {
        repeat(4) {
            launch {
                TaskRunner.test(1000)
            }
        }
    }
}

Thread Phenomenon

DefaultDispatcher-worker-1 | TIMED_WAITING
DefaultDispatcher-worker-2 | TIMED_WAITING
DefaultDispatcher-worker-3 | TIMED_WAITING
DefaultDispatcher-worker-4 | TIMED_WAITING

TaskRunner-Pool-1           | WAITING
TaskRunner-Pool-2           | WAITING

🔍 Execution Chain

Main
 → Dispatchers.IO (Scheduling layer)
   → Default Dispatcher (Relay layer)
     → TaskRunner Pool (Actual execution)

The Essence of the Problem

IO does not perform any business execution here; it only does three things:

👉 But the real work is done by TaskRunner


2. Application Scenario: The Problem Starts to Amplify (Critical)

When the App starts:

class App : Application() {
    val appScope = CoroutineScope(SupervisorJob())
    override fun onCreate() {
        super.onCreate()
        appScope.launch(Dispatchers.IO) {
            repeat(4) {
                launch {
                    TaskRunner.test(1000)
                }
            }
        }
    }
}

What Happens During the Application Phase?

The moment the App starts:

The system automatically triggers:
→ Initializes 4 concurrent tasks

Application Thread Phenomenon

DefaultDispatcher-worker-1 | TIMED_WAITING
DefaultDispatcher-worker-2 | TIMED_WAITING
DefaultDispatcher-worker-3 | TIMED_WAITING
DefaultDispatcher-worker-4 | TIMED_WAITING

TaskRunner-Pool-1           | WAITING
TaskRunner-Pool-2           | WAITING

3. Key Difference: Why Application "Explodes" More?


✔ Characteristic 1: No UI Rhythm Control

ViewModel:

Application:

👉 All tasks are fired instantly


✔ Characteristic 2: Initialization Tasks Are Naturally IO-Intensive

Typical App initialization:

Developer's first reaction:

Dispatchers.IO

✔ Characteristic 3: Deeper Task Nesting

launch(Dispatchers.IO) {
    initA()
    initB()
    initC()
}

Each init might also:


👉 Application directly forms:

IO → IO → IO → Executor

4. Core Problem: Scheduling Redundancy

The real problem is not too many threads, but:

The same task is being "relayed" repeatedly by multiple Dispatchers


❌ Wrong Perception

Dispatchers.IO = Improves performance

❌ What Actually Happens

launch(IO)
 → launch(Default)
   → withContext(executor)

👉 Result:


5. The Most Critical Phenomenon: A Large Number of "Threads That Don't Do Work"

You will see:

DefaultDispatcher-worker-1 | TIMED_WAITING
DefaultDispatcher-worker-2 | TIMED_WAITING
DefaultDispatcher-worker-3 | TIMED_WAITING

❗ What Are These Threads Doing?

The answer is:

👉 They are "relaying tasks," not executing them


6. ViewModel vs Application Comparison

Dimension ViewModel Application
Lifecycle Short Long
Concurrency Method Scattered triggers One-time burst
IO Usage Local Global
Control Capability cancel No cancel
Thread Risk Medium High

👉 The essence of Application:

No rhythm, only a flood peak


7. 🔥 Essential Summary

The real problem is not:

❌ Too much concurrency

But:

You let a system that already has execution capability be scheduled again by Dispatchers.IO


❌ Typical Anti-Pattern

launch(Dispatchers.IO) {
    TaskRunner.test()
}

But TaskRunner itself already has:

withContext(executor)

👉 Equivalent to:

IO scheduling a task that IO has already scheduled


8. ✔ Correct Principles


✔ Principle 1: Do Not Uniformly IO-ify Application

appScope.launch {
    TaskRunner.test()
}

✔ Principle 2: IO Is Only for "Blocking Points"

withContext(Dispatchers.IO) {
    File.read()
}

✔ Principle 3: Avoid IO Wrapping IO

❌ IO → IO → Executor

✔ Principle 4: Execution Model Should Be Pushed Down to the Data Layer

suspend fun test() =
    withContext(executor) { }

9. Conclusion

👉 When the bottom layer is already an "asynchronous execution model," the thread scheduling of the upper scope should be constrained to a small range.

Source code: ScopeSample

This is the scenario in the source code image.png

This is the scenario after removing

launch(Dispatchers.IO)

in the ViewModel and replacing it in Application with

private val applicationScope = CoroutineScope(SupervisorJob() + appExecutor.asCoroutineDispatcher())

image.png

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

用户27910156679

Good code example.