Home > Mobile >  Why can you run a Kotlin coroutine on the main thread?
Why can you run a Kotlin coroutine on the main thread?

Time:02-23

I am having trouble understanding why this piece of code can work properly:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    launch(Dispatchers.Main) {
        log("A")
    }

    log("B")
}

which is supposed to output B first, and then A.

Does this work, because the main thread is already controlled by coroutines? Or does the coroutines API somehow magically inject code into the main thread?

CodePudding user response:

The UI/main thread in Android (and other UI frameworks as well) runs a so called event loop. That means it waits for tasks to be scheduled to it, it has a queue of such tasks and executes them sequentially. For example, when you click on a button, internally onClick action is scheduled to be run on the main thread. But the user is also allowed to schedule their tasks manually, for example by using runOnUiThread() or getMainLooper().

Dispatchers.Main is just yet another way to schedule something on the main thread. It doesn't mean coroutines take full control over the main thread or that they somehow, magically inject anything to it. Main thread is cooperative, it allows scheduling of tasks and coroutines just use this feature.

Also, you asked in the comments, how is it possible that both log statements are run in parallel, but on the same thread. They are not run in parallel. onCreate() only schedules log("A") to be executed later, this is added to the queue. Then log("B") is invoked and only when onCreate() finishes, the main thread can start executing log("A") block. So this is actually sequential, but not in the top-to-bottom order.

CodePudding user response:

Sometimes you need data from an async source. Also you can't proceed without that data like getting logged in user info from Room Database or from online server, In this case you usually block main thread so that you get required data and then proceed further.

CodePudding user response:

Coroutines is a Kotlin concept. And you're not sending your task to "the main thread" but to the Main coroutine Dispatcher. (Although behind the scenes you'll see that indeed you're calling that code from the main thread).

Kotlin is just a programming language that later compiles to different other languages (JVM bytecode for Java and Android, LLVM for native targets and JS for Browser or NodeJS targets).

Regardless of the target the concept remains the same: Kotlin coroutines.

For example: in the JavaScript world we don't have threads: we just have the synchronous and asynchronous task stacks. Same story in case we compiled for embedded devices like Arduino or ESP32's.

For Android coroutines implementation it uses the Executors, Handlers and Looper API's. It doesn't "magically inject itself into the Main thread". But the code you've provided ends up doing something as follows:

Handler(Looper.getMainLooper()).post({/*your lambda here*/})

So the Main coroutine dispatcher on Android will eventually dispatch (pass) a task (your lambda) to the Main Looper on Android.

If that code were to be run on the browser (JavaScript) it'd be a whole other thing at low-level implementation. I imagine it would be something like this (but we'd have to check the source code at github):

new Promise((res) => res(yourLambda()))

But the concept of the coroutines remains the same: We're passing a task to the Main coroutine dispatcher.

Finally, answering your question:

Why can you run a Kotlin coroutine on the main thread?

There's code (or tasks) that are safe to run (or must be run) on the Main thread: like manipulating the properties of a View for instance. To do that we delegate those tasks to the Main coroutine dispatcher that will eventually run your code on the Main thread.

This code is valid to be run on the Main thread; yet it's marked as suspending code. Same happens with your logging statements. They are valid to be run in a suspending context or out of it.

suspend fun makeGone(view: View) {
  view.visibility = View.GONE
}

The fact that a function is marked as suspend does not really matter: it only requires you to run it inside a CoroutineContext or CoroutineScope.

Now with this said: keep in mind that Android will crash if you run IO code from the Main thread:

override fun onCreate() {
  launch(Dispatcher.Main) { somethingThatDoesIO() }
}

This code will crash even if somethingThatDoesIO is not marked as suspend as it's performing IO operations on the Main thread.

if we re-implement the somethingThatDoesIO as follows:

suspend fun somethingThatDoesIO() = withContext(Dispatchers.IO) {
  // old code
}

What's gonna happen is:

  1. You dispatch a task to the Main coroutine dispatcher
  2. The main coroutine dispatcher will execute the lambda
  3. It'll realise that somethingThatDoesIO needs to be run by the IO dispatcher and "hand over everything it needs" (I know this is very high level)
  4. When Dispatchers.IO is done running the task it's going to give the control to the Main dispatcher which will continue with the code from your lambda.

I'm not sure if I helped you out or made things even more confusing. Let me know in the comments

  • Related