Key words: Kotlin coroutine introduction
Assuming you don’t know anything about coroutines, read this article and see if you can understand what coroutines are all about.
1. The introduction
I’ve written about coroutines before, a long time ago. It was painful, after all, because kotlinx. Coroutines was still in its infancy, and I wrote several coroutines articles that basically told you how to write such a framework — which was a terrible feeling, because few people would want it.
This preparation from the coroutine users (that is, programmers you and I he) point of view to write, I hope you can be helpful.
2. Requirement confirmation
Before we get into coroutines, we need to confirm a few things:
- You’ve used threads, right?
- You wrote a callback, right?
- Have you used a similar framework to RxJava?
Look at your answer:
- If you answered “Yes” to all of the above questions, then great, this article is for you, because you’ve realized how scary callbacks can be and found a solution;
- If the first two are “Yes,” that’s fine. At least you’ve started using callbacks. You’re a latent user of the coroutine.
- If only the first one is “Yes,” then you’re probably just starting to learn threads, and you might want to lay the groundwork first
3. A general example
We send a web request via Retrofit with the following interface:
interface GitHubServiceApi {
@GET("users/{login}")
fun getUser(@Path("login") login: String): Call<User>
}
data class User(val id: String, val name: String, val url: String)
Copy the code
Retrofit is initialized as follows:
val gitHubServiceApi by lazy {
val retrofit = retrofit2.Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(GitHubServiceApi::class.java)
}
Copy the code
So when we request the network:
gitHubServiceApi.getUser("bennyhuo").enqueue(object : Callback<User> {
override fun onFailure(call: Call<User>, t: Throwable) {
handler.post { showError(t) }
}
override fun onResponse(call: Call<User>, response: Response<User>){ handler.post { response.body()? .let(::showUser) ? : showError(NullPointerException()) } } })Copy the code
After the request results come back, we switch to the UI thread to present the results. This kind of code is a huge part of our logic. What’s wrong with it?
- With Lambda expressions, we’ve made thread switching less obvious, but it’s still there, and there’s a problem here when developers miss something
- Callbacks are nested at two levels, which may not seem like much, but the logic in a real development environment must be much more complex. For example, a logon failed retry
- Repeated or scattered exception handling logic that we invoke once when the request fails
showError
We called it again when the data read failed. There would have been more duplication in a real development environment
Kotlin’s own syntax already makes this code look so much better that if you were writing it in Java, your intuition would tell you that you’re writing bugs.
If you are not a Android developer, you may not know what’s handler, it doesn’t matter, you can replace with SwingUtilities. InvokeLater {… } (Java Swing), or setTimeout({… }, 0) (Js), and so on.
4. Transform into coroutines
You can certainly make it look like RxJava, but RxJava is much more abstract than coroutines, because unless you’re familiar with those operators, you don’t know what it’s doing (think retryWhen). Coroutines, on the other hand, are, after all, compilers, and they can be very succinct in expressing the logic of the code, regardless of the implementation logic behind it, it will work exactly as your intuition tells you.
For Retrofit, there are two ways to write Retrofit into coroutines, respectively through CallAdapter and suspend functions.
4.1 CallAdapter Mode
Let’s take a look at the CallAdapter approach, which essentially tells an interface method to return a coroutine Job:
interface GitHubServiceApi {
@GET("users/{login}")
fun getUser(@Path("login") login: String): Deferred<User>
}
Copy the code
Note that Deferred is a subinterface of Job.
So we need to add support for Deferred for Retrofit, which requires the open source library:
implementation 'com. Jakewharton. Retrofit: retrofit2 - kotlin coroutines -- adapter: 0.9.2'
Copy the code
When constructing a Retrofit instance, add:
val gitHubServiceApi by lazy {
val retrofit = retrofit2.Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
// Add support for Deferred
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
retrofit.create(GitHubServiceApi::class.java)
}
Copy the code
At this point, we can make a request like this:
GlobalScope.launch(Dispatchers.Main) {
try {
showUser(gitHubServiceApi.getUser("bennyhuo").await())
} catch (e: Exception) {
showError(e)
}
}
Copy the code
Description: Dispatchers.Main is implemented differently on different platforms, if HandlerDispatcher on Android, SwingDispatcher on Java Swing, etc.
This is similar to launching a thread. Launch takes three parameters: the coroutine context, the coroutine launch mode, and the coroutine body:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext, / / context
start: CoroutineStart = CoroutineStart.DEFAULT, // Start mode
block: suspend CoroutineScope. () -> Unit / / coroutines
): Job
Copy the code
Startup mode is not a very complicated concept, but let’s leave it at that and allow scheduling execution directly by default.
Context can do a lot of things, including carrying arguments, intercepting coroutine execution, and so on. In most cases, we don’t need to implement the context ourselves, we just need to use an existing one. An important function of context is thread switching. Dispatchers.Main is an official context, It ensures that the body of the launch coroutine runs in the UI thread (unless you do a thread switch inside the body of the launch coroutine yourself, or start a coroutine running in another context with thread switching capabilities).
In other words, the entire code you see inside the launch in the example is running on the UI thread, and although getUser does switch threads during execution, it cuts back again when it returns the result. This seems a bit confusing, because our intuition tells us that getUser returns a Deferred type, and that its await method returns a User object, meaning that the await needs to wait for the result of the request before it can continue. So doesn’t the await block the UI thread?
The answer is: no. Of course not. What’s the difference between Deferred and Future? Here the await is suspect because it is actually a suspend function that can only be called inside the body of a coroutine or other Suspend function. It is the syntactic sugar of a callback that returns the result through an instance of an interface called a Continuation:
@SinceKotlin("1.3")
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
Copy the code
The source code for 1.3 is actually not very straightforward, although we can look at the source code for Result again, but I don’t want to. What is easier to understand is the previous version of the source code:
@SinceKotlin("1.1")
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resume(value: T)
public fun resumeWithException(exception: Throwable)
}
Copy the code
I’m sure you’ll see in a second that this is actually a callback. If not, compare the Retrofit Callback to:
public interface Callback<T> {
void onResponse(Call<T> call, Response<T> response);
void onFailure(Call<T> call, Throwable t);
}
Copy the code
When a result returns normally, the Continuation calls Resume to return the result, otherwise it calls resumeWithException to throw the exception, exactly as in Callback.
So at this point you should understand that the execution flow of this code is essentially an asynchronous callback:
GlobalScope.launch(Dispatchers.Main) {
try {
//showUser executes after the callback call in the Continuation of the await
showUser(gitHubServiceApi.getUser("bennyhuo").await())
} catch (e: Exception) {
showError(e)
}
}
Copy the code
It’s the dark magic of the compiler that makes the code look synchronous, or what you might call syntax sugar.
In the Java virtual machine (JVM), the signature of the method “await” is not exactly what it looks like:
public suspend fun await(a): T
Copy the code
Its real signature is:
kotlinx/coroutines/Deferred.await (Lkotlin/coroutines/Continuation;) Ljava/lang/Object;Copy the code
This is a function that receives a Continuation instance and returns Object.
// Note that the following code is not correct, only for you to understand the coroutine use
GlobalScope.launch(Dispatchers.Main) {
gitHubServiceApi.getUser("bennyhuo").await(object: Continuation<User>{
override fun resume(value: User) {
showUser(value)
}
override fun resumeWithException(exception: Throwable){
showError(exception)
}
})
}
Copy the code
In the case of await, it is roughly:
// Note that the following is not a real implementation, only for you to understand the coroutine use
fun await(continuation: Continuation<User>): Any {
... // Switch to a non-UI thread and wait for the result to return
try {
val user = ...
handler.post{ continuation.resume(user) }
} catch(e: Exception) {
handler.post{ continuation.resumeWithException(e) }
}
}
Copy the code
This kind of callback can be understood at a glance. With all this in mind, coroutines are no different from callbacks in terms of execution mechanism.
4.2 How to suspend functions
Suspend functions are the only dark magic that the Kotlin compiler supports for coroutines (superficially, and more on that later), and we’ve already gotten a glimpse of them with Deferred await methods. Let’s look at what else we can do with Retrofit.
Retrofit’s current release is 2.5.0 and does not yet support the suspend function. So to try the following code, you need the latest Retrofit source support; Of course, by the time you read this article, Retrofit’s new version will already support this feature.
First we modify the interface method:
@GET("users/{login}")
suspend fun getUser(@Path("login") login: String): User
Copy the code
In this case, Retrofit constructs the Continuation from the interface method declaration and internally encapsulates the Call’s asynchronous request (using enqueue) to get the User instance, as we’ll see later. The use method is as follows:
GlobalScope.launch {
try {
showUser(gitHubServiceApi.getUser("bennyhuo"))}catch (e: Exception) {
showError(e)
}
}
Copy the code
Its execution flow is similar to deferred.await, so we won’t analyze it in detail.
5. What is a coroutine
Well, for those of you who have read this far, you must have been victims of asynchronous code. You must have experienced “callback hell”, which drastically reduced the readability of your code. I have also written a lot of complex asynchronous logic handling, exception handling, which makes your code more repetitive logic; With callbacks and constant thread switching, this doesn’t seem like a big deal, but as the code grows, it can drive you crazy, and it’s not uncommon for exceptions to be reported online due to thread misuse.
Coroutines can help you gracefully get rid of that.
Coroutines themselves are a concept separate from language implementations, and we “rigorously” (ha ha) give the Wikipedia definition:
Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.
Simply put, a coroutine is a non-preemptive or cooperative implementation of concurrent scheduling of a computer program that can be suspended or resumed on its own initiative. Here still need a little knowledge of the operating system, we realized that the thread in Java virtual machine is the realization of the most is mapped to a kernel thread, this means code logic of threads in thread to the CPU time slice can be performed only when, the or, of course it is transparent for our developers; What we often hear about coroutines being lighter is that the coroutine doesn’t map to kernel threads or other heavy resources, its scheduling is done in user mode, and the scheduling between tasks is not preemptive, but collaborative.
On concurrency and parallelicity: Because the CPU time slice is small enough, even a single-core CPU can create the illusion of multitasking, which is called “concurrency.” Parallelism is really running at the same time. With concurrent, it’s more like Magic.
If you’re familiar with the Java virtual Machine, imagine what the Thread class is and why its run method runs in another Thread. Who is responsible for executing this code? Obviously, at first glance, Thread is just an object, and the run method contains the code to execute — nothing more. The same is true of coroutines. If you just look at the standard library API, it will be too abstract, but we explained at the beginning of the coroutines, do not touch the standard library, the kotlinx. Coroutines framework is our users should care about, and the corresponding concept of Thread in the framework is Job. If you look at the definition:
public interface Job : CoroutineContext.Element {.public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
public fun start(a): Boolean
public fun cancel(cause: CancellationException? = null)
public suspend fun join(a). }Copy the code
Let’s look at the definition of Thread:
public class Thread implements Runnable {...public final native boolean isAlive(a);
public synchronized void start(a) {... }@Deprecated
public final void stop(a) {... }public final void join(a) throws InterruptedException {... }... }Copy the code
Here we kindly omit some comments and less relevant interfaces. We find that Thread and Job essentially function the same way. They both carry a piece of code logic (the former through the run method, the latter through the Lambda or function used to construct coroutines), and they both contain the running state of that code.
The difference between the two is only in real scheduling. We only need to know the results of scheduling to make good use of them.
6. Summary
We’re going to start with examples, starting with the code you’re most familiar with, going to examples of coroutines, going to the way coroutines are written, so that you can first get a sense of what a coroutine is, and finally we’re going to give you a definition of what a coroutine can do.
This article does not pursue any internals, but is intended to give you a first impression of how coroutines work. And if you’re still confused, don’t be afraid, I’m going to spend a couple of articles on how coroutines work starting with examples, but the analysis of the principles will come after you’ve mastered coroutines.
Welcome to Kotlin Chinese community!
Chinese website: www.kotlincn.net/
Chinese official blog: www.kotliner.cn/
Official account: Kotlin
Zhihu column: Kotlin
CSDN: Kotlin Chinese community
Nuggets: Kotlin Chinese Community
Kotlin Chinese Community