协程基础

这一部分包括基础的协程概念。

第一个协程程序

A coroutine is an instance of a suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.

Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.

Run the following code to get to your first working coroutine:

  1. import kotlinx.coroutines.*
  2. //sampleStart
  3. fun main() = runBlocking { // this: CoroutineScope
  4. launch { // launch a new coroutine and continue
  5. delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
  6. println("World!") // print after delay
  7. }
  8. println("Hello") // main coroutine continues while a previous one is delayed
  9. }
  10. //sampleEnd

可以在这里获取完整代码。

协程基础 - 图1

代码运行的结果:

  1. Hello
  2. World!

Let’s dissect what this code does.

launch is a coroutine builder. It launches a new coroutine concurrently with the rest of the code, which continues to work independently. That’s why Hello has been printed first.

delay is a special suspending function. It suspends the coroutine for a specific time. Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.

runBlocking is also a coroutine builder that bridges the non-coroutine world of a regular fun main() and the code with coroutines inside of runBlocking { ... } curly braces. This is highlighted in an IDE by this: CoroutineScope hint right after the runBlocking opening curly brace.

If you remove or forget runBlocking in this code, you’ll get an error on the launch call, since launch is declared only on the CoroutineScope:

  1. Unresolved reference: launch

The name of runBlocking means that the thread that runs it (in this case — the main thread) gets blocked for the duration of the call, until all the coroutines inside runBlocking { ... } complete their execution. You will often see runBlocking used like that at the very top-level of the application and quite rarely inside the real code, as threads are expensive resources and blocking them is inefficient and is often not desired.

Structured concurrency

Coroutines follow a principle of structured concurrency which means that new coroutines can only be launched in a specific CoroutineScope which delimits the lifetime of the coroutine. The above example shows that runBlocking establishes the corresponding scope and that is why the previous example waits until World! is printed after a second’s delay and only then exits.

In a real application, you will be launching a lot of coroutines. Structured concurrency ensures that they are not lost and do not leak. An outer scope cannot complete until all its children coroutines complete. Structured concurrency also ensures that any errors in the code are properly reported and are never lost.

Extract function refactoring

Let’s extract the block of code inside launch { ... } into a separate function. When you perform “Extract function” refactoring on this code, you get a new function with the suspend modifier. This is your first suspending function. Suspending functions can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, use other suspending functions (like delay in this example) to suspend execution of a coroutine.

  1. import kotlinx.coroutines.*
  2. //sampleStart
  3. fun main() = runBlocking { // this: CoroutineScope
  4. launch { doWorld() }
  5. println("Hello")
  6. }
  7. // this is your first suspending function
  8. suspend fun doWorld() {
  9. delay(1000L)
  10. println("World!")
  11. }
  12. //sampleEnd

可以在这里获取完整代码。

协程基础 - 图2

Scope builder

In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using the coroutineScope builder. It creates a coroutine scope and does not complete until all launched children complete.

runBlocking and coroutineScope builders may look similar because they both wait for their body and all its children to complete. The main difference is that the runBlocking method blocks the current thread for waiting, while coroutineScope just suspends, releasing the underlying thread for other usages. Because of that difference, runBlocking is a regular function and coroutineScope is a suspending function.

You can use coroutineScope from any suspending function. For example, you can move the concurrent printing of Hello and World into a suspend fun doWorld() function:

  1. import kotlinx.coroutines.*
  2. //sampleStart
  3. fun main() = runBlocking {
  4. doWorld()
  5. }
  6. suspend fun doWorld() = coroutineScope { // this: CoroutineScope
  7. launch {
  8. delay(1000L)
  9. println("World!")
  10. }
  11. println("Hello")
  12. }
  13. //sampleEnd

可以在这里获取完整代码。

协程基础 - 图3

This code also prints:

  1. Hello
  2. World!

Scope builder and concurrency

A coroutineScope builder can be used inside any suspending function to perform multiple concurrent operations. Let’s launch two concurrent coroutines inside a doWorld suspending function:

  1. import kotlinx.coroutines.*
  2. //sampleStart
  3. // Sequentially executes doWorld followed by "Done"
  4. fun main() = runBlocking {
  5. doWorld()
  6. println("Done")
  7. }
  8. // Concurrently executes both sections
  9. suspend fun doWorld() = coroutineScope { // this: CoroutineScope
  10. launch {
  11. delay(2000L)
  12. println("World 2")
  13. }
  14. launch {
  15. delay(1000L)
  16. println("World 1")
  17. }
  18. println("Hello")
  19. }
  20. //sampleEnd

可以在这里获取完整代码。

协程基础 - 图4

Both pieces of code inside launch { ... } blocks execute concurrently, with World 1 printed first, after a second from start, and World 2 printed next, after two seconds from start. A coroutineScope in doWorld completes only after both are complete, so doWorld returns and allows Done string to be printed only after that:

  1. Hello
  2. World 1
  3. World 2
  4. Done

An explicit job

A launch coroutine builder returns a Job object that is a handle to the launched coroutine and can be used to explicitly wait for its completion. For example, you can wait for completion of the child coroutine and then print “Done” string:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. //sampleStart
  4. val job = launch { // launch a new coroutine and keep a reference to its Job
  5. delay(1000L)
  6. println("World!")
  7. }
  8. println("Hello")
  9. job.join() // wait until child coroutine completes
  10. println("Done")
  11. //sampleEnd
  12. }

可以在这里获取完整代码。

协程基础 - 图5

This code produces:

  1. Hello
  2. World!
  3. Done

协程很轻量

Coroutines are less resource-intensive than JVM threads. Code that exhausts the JVM’s available memory when using threads can be expressed using coroutines without hitting resource limits. For example, the following code launches 50,000 distinct coroutines that each waits 5 seconds and then prints a period (‘.’) while consuming very little memory:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. repeat(50_000) { // 启动大量的协程
  4. launch {
  5. delay(5000L)
  6. print(".")
  7. }
  8. }
  9. }

可以在这里获取完整代码。

协程基础 - 图6

If you write the same program using threads (remove runBlocking, replace launch with thread, and replace delay with Thread.sleep), it will consume a lot of memory. Depending on your operating system, JDK version, and its settings, it will either throw an out-of-memory error or start threads slowly so that there are never too many concurrently running threads.