I recently read the source code of Coroutines.
It’s pretty fun, and I’ve learned a lot from the source code. I am going to share some I learned here. I would break the learning into several parts. I hope everyone can find something useful for your projects.
I download the latest master branch code at HERE, and if you are interested, you can just download the official release version to try. So, it’s up to you to choose either the official release version or the latest master branch. And the Kotlin version is 1.3.61
check README here.
Part1 - Introduction
Before starting the introduction, let’s think of a question: how do you handle the CPU-consuming tasks or networking accessing works? We can’t do these tasks in the main thread (in Android, which is UIThread). Then we need to think about using the multiple threading techniques to solve our questions. In Java, you can use Thread
to do this.
1 | Runnable task = () -> { |
You can see the result:
1 | Hello Thread-0 |
or
1 | Done! |
But creating threads frequently is a resource-consuming behavior, you would want to create a ThreadPool
to reduce the cost.
In Android, we have several options for concurrency tasks. For example, you can use the AsyncTask,
Background Service,
and Handler
in the Android projects.
The second question is: how do you communicate between two different threads? In Java, you need to manipulate the Thread
interaction by using IPC
mechanism, for example, shared memory, Pipe, named Pipe, mapped memory, semaphore, socket, message, and signals. There are many techniques we can use. What about concurrency in Kotlin’s world? Do we have any weapons for arming? Fortunately, we can still use the same techniques from the Java world. But Kotlin gives us a more powerful weapon, Coroutines, to use.
Let’s see an example first. We have three functions which need the results from the previous one finished the job.
1 | fun requestToken(): Token { ... } |
These three functions are probably time-consuming tasks. Hence, it’s not able to put them in the UI thread in the Android project. What can we do now?
By using a Thread
As I mentioned before, create a thread to run it.
1 | Thread({ |
Keeping this in mind: thread is not cheap. You would need to do context-switch, which is costly.
By using a Callback
We can also use a callback function plus threading to catch the result from each function.
1 | fun postItem(item: Item) { |
What’s the drawback of using callback? The most known pitfall is the callback hell.
That makes you hardly debug and read the source code.
By using Java Future
In Java 8, it introduces the CompletableFuture.
This feature lets you connect several tasks.
1 | fun requestToken(): CompletableFuture { ... } |
Rx Extension
I believe there must be many developers who use this library.
1 | Single.fromCallable { requestToken() } |
I think the biggest problem for me when using Rx is that the exception log is pretty hard to read. And it also creates too many instances that Rx needs and consumes too much memory.
Here’s why Coroutines show and want to give developers another solution.
Coroutines
1 | suspend fun requestToken(): Token { ... } |
It looks just like the previous code snap, doesn’t it? If we want to add a try-catch
block inside the postItem
function. Just add it. We don’t need any extra effort to handle it.
First of all, let’s check how a coroutine works. A Coroutine
is like a Thread
in concept. But it’s much more lightweight than a real Thread.
Several different coroutines can run on a Thread,
and the Coroutine runtime manages them. You might remember the thread itself is not that cheap as you think because the system controls the threads, and it needs to do context-switch for you.
Another reason that a thread is costly is because it would spend more resource on the creation and termination. So the good practice is to use a thread pool (to create some threads inside first) in advance. Let’s back to Coroutines.
The Coroutines
don’t have the same problem. It’s very lightweight, which means you can create a Coroutine anytime, anywhere. It’s controlled by Kotlin runtime, not by system.
What’s the Suspend
keyword? You would call suspending functions in the coroutines, just like regular functions. To call a suspending function, you need to declare the caller function as a suspending function as well. A suspending function can only be used in a coroutine or another suspending function.
When you try to launch a coroutine, it would be like this:
1 | GlobalScope.launch { |
Check the launch function itself:
1 | public fun CoroutineScope.launch( |
It launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job.
You can use this Job
object to cancel the coroutine execution by calling Job.cancel().
A CoroutineScope defines a scope for new coroutines. It contains a CoroutineContext
variable, which describes what factors would be involved in the CoroutineScope by setting up the Element.
Which thread would host a specific coroutine? In launch function, if you don’t assign a dispatcher, it would use Dispatchers. Default
.
1 | public fun CoroutineScope.launch( |
in newCoroutineContext
, it will use Dispatchers.Default
to find a thread to host a coroutine. There are several Dispatchers types:
1 | public actual val Default: CoroutineDispatcher |
In the launch function, if the context does not have any dispatcher nor any other ContinuationInterceptor,
then Dispatchers. Default
would be used. The Default
is a CoroutineDispatcher,
which has a shared thread pool to use.
What’s a Job
? The most basic instances of Job
are created with launch
function of coroutine builder or with a Job()
factory function. By default, a failure of any of the job’s children leads to an immediate failure of its parent and cancellation of the rest of its children. This behavior can be customized using SupervisorJob
. Job has a simple life-cycle:
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | false |
false |
false |
Active (default initial state) | true |
false |
false |
Completing (transient state) | true |
false |
false |
Cancelling (transient state) | false |
false |
true |
Cancelled (final state) | false |
true |
true |
Completed (final state) | false |
true |
false |
It has three different states: isActive,
isCompleted,
and isCancelled.
Conceptually, execution of the job does not produce a result value. Jobs are launched solely for their side-effects. If you try to get a result from a Job,
check the Deferred.
Here I introduce the basic concepts of Coroutines, which is very powerful and helps to reduce the complexity of multi-task in Kotlin world.
Happy coding, enjoy.
Part2 - Suspend, Resume and Dispatch
Part3 - Callback, Interaction and Cancellation
Part4 - Exception
Part5 - Concurrency
Reference
- KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov
- Asynchronous Programming Techniques