How does Kotlin coroutines manage to schedule all coroutines on the main thread without block it?

Viewed 320

I've been experimenting with the Kotlin coroutines in android. I used the following code trying to understand the behavior of it:

fun onStartButtonPressed(view: View) {
    Log.d(TAG, "Outside Scope: ${Thread.currentThread().id}")

    lifecycle.coroutineScope.launch {
        Log.d(TAG, "Top Level Scope: ${Thread.currentThread().id}")

        val t1 = launch {
            Log.d(TAG, "Task 1 Scope: ${Thread.currentThread().id}")
            for (i in 1..2) {
                delay(1000L)
                withContext(Dispatchers.Main) {
                    viewBinding.task1ProgressBar.progress = i * 50
                }
            }
        }

        val t2 = launch {
            Log.d(TAG, "Task 2 Scope: ${Thread.currentThread().id}")
            for (i in 1..4) {
                delay(1000L)
                withContext(Dispatchers.Main) {
                    viewBinding.task2ProgressBar.progress = i * 25
                }
            }
        }

        t1.join()
        t2.join()

        withContext(Dispatchers.Main) {
            Log.d(TAG, "Completion Scope: ${Thread.currentThread().id}")
            viewBinding.startButton.isEnabled = true
            viewBinding.resetButton.isEnabled = true
            viewBinding.statusTextView.text = "All tasks have been finished."
        }
    }

    viewBinding.statusTextView.text = "All tasks have been started."
    viewBinding.startButton.isEnabled = false
    viewBinding.resetButton.isEnabled = false
}

The output of the code is:

2021-06-15 21:05:13.066 18079-18079/com.demo.coroutinedemo D/MainActivity: Outside Scope: 2
2021-06-15 21:05:13.132 18079-18079/com.demo.coroutinedemo D/MainActivity: Top Level Scope: 2
2021-06-15 21:05:13.140 18079-18079/com.demo.coroutinedemo D/MainActivity: Task 1 Scope: 2
2021-06-15 21:05:13.142 18079-18079/com.demo.coroutinedemo D/MainActivity: Task 2 Scope: 2
2021-06-15 21:05:17.189 18079-18079/com.demo.coroutinedemo D/MainActivity: Completion Scope: 2

This is very wired to me as join() method would block the main thread but the UI is not freezing, why is that?

1 Answers

This is exactly the reason why coroutines were invented and how they differ from threaded concurrency. Coroutines don't block, but suspend (well, they can do both). And "suspend" isn't just another name for "block". When they suspend (e.g. by invoking join()), they effectively free the thread that runs them, so it can do something else somewhere else. And yes, it sounds like something that is technically impossible, because we are in the middle of executing the code of some function and we have to wait there, but well... welcome to coroutines :-)

You can think of it as the function is being cut into two parts: before join() and after it. First part schedules the background operation and immediately returns. When background operation finishes, it schedules the second part on the main thread. This is not how coroutines works internally (functions aren't really cut, they create continuations), but this is how you can easily imagine them working if you are familiar with executors or event loops.

delay() is also a suspending function, so it frees the thread running it and schedules execution of the code below it after a specified duration.

Related