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

3

13

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?

Oaken answered 22/2, 2022 at 13:11 Comment(1)
I always assume Dispatchers were something else, but reading the documentation (via IDE) it explicitly says it will run on the main thread where UI manipulation is available, this is very interesting.Upward
T
23

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.

Tombola answered 22/2, 2022 at 15:19 Comment(4)
So it's a framework thing. If I were to write a simple JVM executable like fun main() { /* stuff */ } then inside "stuff" there wouldn't be a way to make a coroutine run on the main thread without using runBlocking blocking the entire thread?Oaken
I'm not sure if I get your question right, but yes, I believe runBlocking() is the only way to use existing thread for coroutines. We can't "force" any thread to do something we need, we have to occupy it entirely by running the event loop. Internally runBlocking() does what I described. It creates a queue of tasks and it loops over this queue. From the external observer the thread is entirely blocked, from the internal observer it allows to schedule tasks on it and it runs them.Tombola
Also, I think there is a common misconception on what is the "main" thread. This is not some magic thread that could be only one running inside the application. Main thread is just the main thread of an UI framework, thread that spins its main event loop (it could have more). If for any reason you use 2 UI frameworks in a single application, then each of them will have its own main thread. And still, it doesn't have to be the same main thread as the one that started the application in the main() function.Tombola
"I believe runBlocking() is the only way to use existing thread for coroutines" - or if the thread is already running some kind of event loop or allows to schedule tasks to it, we can use this functionality and schedule coroutines on it. We can for example schedule coroutines on a Java Executor or on Android main looper. However, we can't schedule coroutines on just any thread, it has to cooperate.Tombola
S
12

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 😅

Straiten answered 22/2, 2022 at 13:52 Comment(2)
Thank you! This does help, if you pair it with the fact from broot's answer below, that Android's "Main" thread is actually an event loop to which you can schedule tasks. So it's not exactly the main thread in a traditional sense (thread which runs the executable), but rather a task executor thread used for the main tasks, which are probably scheduled by the actual main thread hidden in the implementation. Right?Oaken
That's right! :)Straiten
V
-3

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.

Vender answered 22/2, 2022 at 13:15 Comment(3)
OP is not asking why you need to use coroutines, is asking why coroutines can run on the main thread.Upward
@Upward thats what answered above...Vender
No, he's asking why it CAN work, not why on should or should not do it.Steradian

© 2022 - 2024 — McMap. All rights reserved.