How to make sense of Kotlin coroutines

sunbird89629發表於2019-03-28

【翻譯中】 proandroiddev.com/how-to-make…

Coroutines are a great way to write asynchronous code that is perfectly readable and maintainable. Kotlin provides the building block of asynchronous programming with a single language construct: the suspend keyword, along with a bunch of library functions that make it shine.

In this post, I’ll try to explain in simple words the basics of coroutines and suspending functions. In order to keep it short, I wont dive into advanced constructs based on coroutines. The point is rather to give an overview and share my mental model of coroutines.

What is a coroutine? The Kotlin team defines coroutines as “lightweight threads”. They are sort of tasks that the actual threads can execute. The banner on Kotlinlang.org is an illustration of this :

The most interesting thing is that a thread can stop executing a coroutine at some specific “suspension points”, and go do some other work. It can resume executing the coroutine later on, or another thread could even take over.

So, to be more accurate, one coroutine is not exactly one “task” but rather a sequence of “sub-tasks” to execute in a specific, guaranteed order. Even if the code seems to be in one sequential block, each call to a suspending function delimits the start of a new “sub-task” within the coroutine.

This brings us to the meat of the subject: suspending functions.

Suspending functions You may find functions like kotlinx’s delay or Ktor’s HttpClient.post that need to wait for something or do intensive work before returning, and are marked with the suspend keyword.

suspend fun delay(timeMillis: Long) {...} suspend fun someNetworkCallReturningValue(): SomeType { ... } These are called suspending functions. As we’ve just seen:

Suspending functions may suspend the execution of the current coroutine without blocking the current thread. This means that the code you are looking at might stop executing at the moment it calls a suspending function, and will resume at some later time. However, it doesn’t say anything about what the current thread will do in the meantime.

It might go back to executing another coroutine at that point, and it could later resume executing the coroutine we left. All of this is controlled by how your suspending function is called by the non-suspending functions world, but there is nothing inherently asynchronous about suspending functions.

Suspending functions are only asynchronous if they are explicitly used as such. We will see this later on. But for now, you can simply consider suspending functions as special functions that declare they take some time. And keep in mind that they implicitly split your functions into sub-tasks, without worrying yet about the intricacies of threads and dispatching. That’s actually why they’re great, you don’t need to worry about that when you’re inside.

The suspending world is nicely sequential You have probably noticed that suspending functions don’t have special return types. They are really declared just like usual functions. We don’t need any wrapper like Java’s Future or JavaScript's Promise. This insists on the fact that suspending functions are not asynchronous themselves, unlike JavaScript’s async functions, which return promises.

From inside a suspending function, we can reason sequentially about function calls This is what makes asynchronous stuff easy to reason about in Kotlin. Inside a suspending function, calls to other suspending functions behave like normal function calls: we need to wait for the execution of the called function before getting the return value and executing the rest of the code.

That is what will allow us to write complex asynchronous code in a simple way later on.

Bridging the normal world and the suspending world Calling a suspending function from a “normal” function directly cannot compile. The usual explanation is “because only coroutines can be suspended”, and from there we conclude that we need to somehow create a coroutine from which to run our suspending function. Great. But why?

Conceptually, suspending functions sort of announce from their declaration that they may “take some time to execute”. If you’re not a suspending function yourself, this forces you to do one of 2 things explicitly:

actually block the thread while you’re waiting (like a normal, synchronous, function call) start something asynchronous to do it for you, and return immediately (which can be done in various possible ways) You can see the creation of a coroutine as a way to express your choice, which must be explicit (and this is great!). This is done by using functions called coroutine builders.

Coroutine builders Coroutine builders are simple functions that create a new coroutine to run a given suspending function. They can be called from normal non-suspending functions because they are not suspending themselves,and thus act as a bridge between the normal and the suspending world.

The Kotlin standard library contains multiple coroutine builders to do various things. We will see a few in the following subsections.

Block the current thread with “runBlocking” The simplest way to deal with a suspending function from a normal function is to just block the current thread and wait. The coroutine builder to block the current thread is called runBlocking :

In the context of runBlocking, the given suspending function and its children in the call hierarchy will effectively block the current thread until it finishes executing.

As you can see from its signature, the function passed to runBlocking is a suspending function, even though runBlocking itself is not suspending (it is thread-blocking):

fun runBlocking( ..., block: suspend CoroutineScope.() -> T ): T { ... } This is often used from the main() function to give a sort of top-level coroutine from which to work, and keep the JVM alive while doing so (we will see that in the section about structured concurrency).

Fire-and-forget with “launch” Usually, the point of coroutines is not to block the thread, but rather start an asynchronous task. The coroutine builder called launch allows to start a coroutine in background and keep working in the meantime.

From the Kotlin documentation, we have this example:

The comments should speak for themselves. This will print “Hello,” immediately, and add “World!” after a second.

Note that, for the purpose of the example, we need to somehow block the main function anyway in order to see what happens with launch. That’s why they are re-using runBlocking here, just to keep the JVM alive. (We could have used Thread.sleep() but that wouldn’t be very Kotlin-esque now, would it?)

Don’t worry about this GlobalScope object, I’ll get to it in just a minute.

Get a result asynchronously with “async” Here is another coroutine builder called async which allows to perform an asynchronous operation returning a value:

In order to get the result of the deferred value, async returns a convenient Deferred object, which is the equivalent of Future or Promise. We can call await on this deferred value in order to wait and get the result.

await is not a normal blocking function, it is a suspending function. This means we can’t just call it from main(). We need to somehow actually block the main function in order to wait for the result, so we use runBlocking here to wrap the call to await.

The sharp eyes may have noticed the GlobalScope again here, so I’m afraid I have to talk about it now. This is the tool that allows us to create a hierarchy of coroutines. This is what the Kotlin team calls structured concurrency.

Structured concurrency If you have followed the few examples above, you may have noticed that we needed to go through the classic “block and wait for my coroutines to finish” pattern.

In Java, this is usually obtained by keeping references to threads and calling join on all of them in order to block the main thread while waiting for all the others. We could do a similar thing with Kotlin coroutines, but this is not idiomatic at all.

In Kotlin, coroutines can be created in a hierarchy, which allows a parent coroutine to automatically manage the life cycle of its child coroutines for you. It can for instance wait for its children to complete, or cancel all its children if an exception occurs in one of them.

Creating a hierarchy of coroutines Except for runBlocking, which should not be called from a coroutine, all coroutine builders are declared as extensions of the CoroutineScope class, to encourage people to structure their coroutines:

fun runBlocking(...): T {...}fun CoroutineScope.async(...): Deferred {...} fun CoroutineScope.launch(...): Job {...} fun CoroutineScope.produce(...): ReceiveChannel {...} ... In order to create a coroutine, you either need to call these builders on the GlobalScope (creating a top-level coroutine) or from an already existing coroutine scope (creating a child coroutine of that scope). In fact, if you write a function that creates coroutines, you should declare it as an extension of the CoroutineScope class too. This is a convention that also happens to allow you to call coroutine builders easily, because a CoroutineScope is available to you as this.

If you take a look at coroutine builders’ signatures, you may notice that the suspending function they take as a parameter is also defined as an extension function of the CoroutineScope class:

fun CoroutineScope.async( ... block: suspend CoroutineScope.() -> T ): Deferred { ... } This means we can call other coroutine builders inside of that function without specifying any receiver, and the implicit receiver will be the child scope of the current coroutine, making it act as a parent. Easy!

Here is how we should structure the previous examples in a more idiomatic way:

Note that we don’t need GlobalScope anymore, because a scope is provided by the wrapping runBlocking call. Neither do we need extra delays here to wait for child coroutines to finish. The runBlocking will wait for all its children to finish before finishing its own execution, and so the main thread will stay blocked as well, by definition of runBlocking.

The coroutineScope builder You may have noticed that the use of runBlocking is discouraged from inside coroutines. This is because the Kotlin team wants to avoid thread-blocking functions inside coroutines, and uses suspending operations instead. The suspending equivalent of runBlocking is the coroutineScope builder.

coroutineScope simply suspends the current coroutine until all child coroutines have finished their execution. Here is the example directly taken from the Kotlin documentation:

Beyond the basics The basic building blocks I explained here are really not the greatest aspects of the coroutines concept in Kotlin. We can make use of coroutines to express concurrent stuff really nicely by using channels, producers and consumers etc. But I believe we first need to understand these building blocks before starting building higher abstractions on top of them.

There is a lot to say about coroutines, and this article barely scratches the surface of course, but I hope this post helped you understand coroutines and suspending functions better.

Please let me know if it was helpful to you, if you would like to know more about a particular aspect. Don’t hesitate to point out mistakes if you see any.

相關文章