Reading Coroutine official guide thoroughly — Part 1 — Dive 2
Deep dive into code level
[Korean] English
(Return to the table of content)
What is the reason that Coroutine is called light-weight thread?
When a coroutine is newly created and executed, there would be a new thread for it or not. It means that there is no correlation between the two. In fact, you can consider Coroutine as an executable code block or a chain of them. The flow diagram shown below is drawn after analyzing coroutine code for your easy understanding of the process of Coroutine execution.

Look at the most left part. There is a CoroutineScope. Coroutines should be included in a particular coroutine scope in order to be executed. The CoroutineContext which is owned by the CoroutineScope (Blue one) has the UI Dispatcher as its dedicated dispatcher. It means all of the functions that are executed in this scope are dispatched to the UI Thread. (i.e. being executed on the UI Thread).
Now that a coroutine has been created in the coroutine scope (purple image in the center). This coroutine inherited context from the scope and only overrides dispatcher (which is one of the elements of CoroutineContext) as ThreadPoolDispatcher. (Basically, if there are no overridden element, all of the elements from the scope or parent context are inherited). From now on, all of the functions which are going to be executed in this coroutine are running in the background thread as using the ThreadPoolDispatcher.
In case of executing GlobalScope.launch { } builder, the last code block (lambda parameter) is transformed as a type of Continuation. Continuation is coming from CPS(Continuation Passing Style).
TL; DR : A chain of function calls for doing some task usually uses return value of each function. However in case of CPS, it adds one more parameter to each function and it is called as Continuation(similar to Callback). Each function invokes continuation function with the result value instead of returning it. This can facilitate to change dispatcher per Continuation or postpone the execution of each continuation.
The continuation which is transformed from the code block is initially created with the suspended state and changes its state to resumed when resuming is requested by calling resume() function. At this point, it asks dispatcher whether it needs to be dispatched (change to the other thread) or not by calling isDispatchedNeeded() function. If it needs to do that, it is dispatched to the applicable thread and executed on it.
In this example, I have used different dispatcher between CoroutineScope and created Coroutine. But what if I don’t override any context elements from the CoroutineScope and I just use it as it is? Then, this coroutine call is executed like a regular function call. This is the reason why Coroutine is called as a light-weight thread. The thread for execution of a coroutine can be determined by Dispatcher but the coroutine as itself don’t construct or change its executable environment.
For this reason, the example shown below can be executed without OOM Exception.
As you can see in the above example, the code executes 100,000 coroutines. In the code, launch { } coroutine builder uses dispatcher which come from current scope because it doesn’t specify any Dispatcher elements. Current scope is generated from runBlocking coroutine builder and it uses GlobalScope internally. The Dispatcher uses BlockingEventLoop that has a serial queue for the event loop mechanism. For this implementations, this example generates 100,000 events for the event loop on the execution thread and prints. (dot) 100,000 times.