Key words: Kotlin coroutine startup mode

Now that you know how coroutines work, you might want to try one yourself. This article will give you a detailed look at the differences between the several startup modes of coroutines, but I’m not going to dive into the source code parsing right now. You just need to keep these rules in mind to use coroutines well.

1. Think back to when you first learned Thread

I’m sure that most of the developers who come into contact with Kotlin today have a Java foundation, and when we first started learning Thread, we all did something like this:


val thread = object : Thread(){
    override fun run(a) {
        super.run()
        //do what you want to do.
    }
}

thread.start()

Copy the code

Someone must have forgotten to call start and wondered why my thread didn’t start. To be honest, the start design of this thread is actually quite strange, but I understand the designers, after all, stop was still available, and they quickly realized that stop was a mistake. It was deprecated in JDK 1.1 because it was unsafe.

Since stop is a mistake, is it also a mistake to always make beginners lose start?

Ah, that’s a little off topic. We’re going to focus on Kotlin today. The designers of Kotlin have been very thoughtful and have provided a convenient method for threading:


val myThread = thread {
    //do what you want
}

Copy the code

The thread method has a parameter start that defaults to true. In other words, the created thread is started by default, unless you really don’t want it to work right away:


val myThread = thread(start = false) {
    //do what you want
}

//later on ...
myThread.start()

Copy the code

It looks a lot more natural. Interfaces should be designed so that the default values meet 80% of the requirements.

2. Take a look at the coroutine launch

The reason why I said so many threads, after all, you are most familiar with it. The coroutine API design is in line with this. Let’s look at the simplest way to start a coroutine:


GlobalScope.launch {
    //do what you want
}

Copy the code

So how does this code execute? As we said, starting a coroutine requires three things: the context, the launch mode, and the coroutine body, which is like the code in Thread.run, needless to say.

This article will describe the startup mode in detail. In the Kotlin coroutine, the startup mode is an enumeration:


public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi
    ATOMIC,
    @ExperimentalCoroutinesApi
    UNDISPATCHED;
}

Copy the code
model function
DEFAULT Execute coroutine body immediately
ATOMIC Executes the coroutine body immediately, but cannot cancel before it starts running
UNDISPATCHED The body of the coroutine is executed immediately on the current thread until the first suspend call
LAZY Run only if needed

2.1 the DEFAULT

The most common of the four startup modes are actually DEFAULT and LAZY.

DEFAULT is a hunger-type launch. After launch is called, it enters the pending state immediately, and execution can begin once the scheduler is OK. Let’s look at a simple example:


suspend fun main(a) {
    log(1)
    val job = GlobalScope.launch {
        log(2)
    }
    log(3)
    job.join()
    log(4)}Copy the code

Description: The main function supports suspend since Kotlin 1.3. In addition, the omission of arguments to the main function is a feature of Kotlin 1.3. The following examples are run directly from the suspend Main function without specific notice.

On the JVM, the default scheduler is implemented in a similar way to other languages, with dedicated threads in the background that handle asynchronous tasks. Therefore, the result of the above application might be:

19:51:08:160 [main] 1
19:51:08:603 [main] 3
19:51:08:606 [DefaultDispatcher-worker- 1] 2
19:51:08:624 [main] 4
Copy the code

It could also be:

20:19:06:367 [main] 1
20:19:06:541 [DefaultDispatcher-worker- 1] 2
20:19:06:550 [main] 3
20:19:06:551 [main] 4
Copy the code

This depends on the order in which the CPU schedules the current and background threads, but don’t worry, you’ll soon see that the order in which 2 and 3 are output in this example is not that important.

The default scheduler implementation on the JVM, as you might have guessed, is to open a pool of threads, but a few threads are enough to schedule thousands of coroutines, each with its own call stack, which is fundamentally different from simply opening a pool of threads to perform asynchronous tasks.

Of course, we say that Kotlin is a cross-platform language, so the above code can also run in a JavaScript environment, such as Nodejs. In Nodejs, the default scheduler for Kotlin coroutines does not switch threads, and the output is slightly different, which seems more in line with JavaScript execution logic.

We’ll talk more about schedulers later.

2.2 LAZY

LAZY is a LAZY start. There is no scheduling behavior after launch, and the coroutine body naturally does not go into execution until we need it to execute. This is actually a little bit confusing, what do we mean when we need it to execute? In this case, we can use the following method:

  • callJob.start, which actively triggers the scheduling execution of coroutines
  • callJob.join, implicitly triggers the scheduled execution of the coroutine

So this “need” is actually an interesting phrase, and you’ll see that we can also express the need for Deferred with await. This behavior is different from thread. join, which has no effect if it is not started.


log(1)
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
    log(2)
}
log(3)
job.start()
log(4)

Copy the code

Based on this, for the above example, the output might be:

14:56:28:374 [main] 1
14:56:28:493 [main] 3
14:56:28:511 [main] 4
14:56:28:516 [DefaultDispatcher-worker-1] 2
Copy the code

Of course, if you are lucky enough, it is possible to be 2 to 4 in front. And for join,

. log(3)
job.join()
log(4)

Copy the code

Because we are waiting for the coroutine to finish executing, the output must be:

14:47:45:963 [main] 1
14:47:46:054 [main] 3
14:47:46:069 [DefaultDispatcher-worker-1] 2
14:47:46:090 [main] 4
Copy the code

2.3 ATOMIC

ATOMIC only makes sense when it comes to cancel, and cancel itself is a topic worth discussing in detail, but here we simply assume that the coroutine is cancelled after cancel, that it is no longer executed. The results will vary depending on the timing of the call to cancel, such as before the coroutine is scheduled, before the coroutine is scheduled but not yet executed, has started execution, has finished execution, and so on.

To see how this differs from DEFAULT, let’s look at an example:


log(1)
val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
    log(2)
}
job.cancel()
log(3)

Copy the code

Cancel immediately after we create the coroutine, but since the coroutine is in ATOMIC mode, it must be scheduled, so 1, 2, and 3 must all be printed, but the order of 2 and 3 is unclear.

20:42:42:783 [main] 1
20:42:42:879 [main] 3
20:42:42:879 [DefaultDispatcher-worker-1] 2
Copy the code

In DEFAULT mode, if cancel is already called when the coroutine is scheduled for the first time, then the coroutine will be canceled without any call. It is also possible that the coroutine has not been canceled when it starts, so it can start normally. So if the previous example is in DEFAULT mode, the 2 May or may not be printed.

Note that the cancel call definitely sets the job’s state to cancelling, but the ATOMIC mode coroutine ignores this state when it starts. To prove this, let’s make the example a little more complicated:


log(1)
val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
    log(2)
    delay(1000)
    log(3)
}
job.cancel()
log(4)
job.join()

Copy the code

We added a delay between 2 and 3, the delay will cause the execution of the coroutine body to be suspended, and the later part will be scheduled 1000ms later, so 3 will be output 1000ms after the execution of 2. For ATOMIC mode, we’ve already discussed that it must be started, and in fact it doesn’t stop until the first suspend start is encountered. Delay is a suspend function, so our coroutine has its first suspend start, and it happens that delay supports cancel. So the next 3 will not be printed.

We have a similar problem with stopping tasks in threads, but unfortunately the stop interface, which looks similar to Cancel in threads, has been deprecated due to safety issues. But as we dig deeper, you’ll see that the cancel of coroutines is more like the interrupt of threads in a sense.

2.4 UNDISPATCHED

With that in mind, UNDISPATCHED is easy to understand. In this mode, the dispatched coroutine starts executing directly on the current thread up to the first hanger point. This sounds a bit like the previous ATOMIC point, except that UNDISPATCHED starts executing the body without going through any scheduler. Of course, the execution after the mount point is encountered depends on the logic of the mount point itself and the scheduler in the context.


log(1)
val job = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
    log(2)
    delay(100)
    log(3)
}
log(4)
job.join()
log(5)

Copy the code

Let’s consider the UNDISPATCHED mode as an example. According to the previous discussion, the coroutine will be executed in the current thread immediately after it is started. Therefore, 1 and 2 will be executed in the same thread continuously. Join requires waiting for the coroutine to finish, so wait for output from 3 before executing 5. Here are the results:

22:00:31:693 [main] 1
22:00:31:782 [main @coroutine2 # 1]
22:00:31:800 [main] 4
22:00:31:914 [DefaultDispatcher-worker-1 @coroutine3 # 1]
22:00:31:916 [DefaultDispatcher-worker-1 @coroutine5 # 1]
Copy the code

Inside the square brackets is the thread name, and we noticed that when the coroutine executes, it changes the thread name to make itself more visible. Another detail that may seem confusing is that the thread of 5 after join is the same as that of 3. Why is this? As we mentioned earlier, our examples all run in suspend Main, so suspend main helps us directly launch a coroutine, and our coroutines are subcoroutines, so the scheduling of 5 depends on the scheduling rules for the outermost coroutine. We’ll talk about scheduling coroutines later.

3. Summary

In this article, some examples will be given to reveal the coroutine gradually. I believe you read for the execution mechanism of the coroutine has a general understanding, at the same time for the coroutine scheduling this topic must also be very curious or confused, this is normal — because we haven’t talked about it, rest assured, the content of the scheduler has been arranged:).

The appendix

Log function definition:


val dateFormat = SimpleDateFormat("HH:mm:ss:SSS")

val now = {
    dateFormat.format(Date(System.currentTimeMillis()))
}

fun log(msg: Any?). = println("${now()} [${Thread.currentThread().name}] $msg")

Copy the code

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