编辑本页

目录

协程基础

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

你的第一个协程程序

运行以下代码:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L) // 无阻塞的等待1秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Hello,") // 主线程的协程将会继续等待
    Thread.sleep(2000L) // 阻塞主线程2秒钟来保证 JVM 存活
}

你可以点击这里获得完整代码

代码运行的结果:

Hello,
World!

本质上,协程是轻量级的线程。 它们在 CoroutineScope 上下文中和 launch 协程构建器 一起被启动。 这里我们在 GlobalScope 中启动了一些新的协程,存活时间是指新的协程的存活时间被限制在了整个应用程序的存活时间之内。

你可以使用一些协程操作来替换一些线程操作,比如: 用 GlobalScope.launch { …… } 替换 thread { …… }delay(……) 替换 Thread.sleep(……)。 尝试一下。

如果你开始使用 GlobalScope.launch 来替换 thread,编译器将会抛出错误:

Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

这是因为 delay 是一个特别的 挂起函数 ,它不会造成线程阻塞,但是 挂起 函数只能在协程中使用。

桥接阻塞与非阻塞的世界

第一个例子中在同一段代码中包含了 非阻塞的 delay(……)阻塞的 Thread.sleep(……)。 这非常容易让我们记混哪个是阻塞的、哪个是非阻塞的。 来一起使用显式的阻塞 runBlocking 协程构建器:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主线程中的代码会立即执行
    runBlocking {     // 但是这个函数阻塞了主线程
        delay(2000L)  // ……我们延迟2秒来保证 JVM 的存活
    } 
}

你可以点击这里来获得完整代码

结果是相似的,但是这些代码只使用了非阻塞的函数 delay。 在主线程中调用了 runBlocking阻塞 会持续到 runBlocking 中的协程执行完毕。

这个例子可以使用更多的惯用方法来重写,使用 runBlocking 来包装 main 函数:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // 开始执行主协程
    GlobalScope.launch { // 在后台开启一个新的协程并继续
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主协程在这里会立即执行
    delay(2000L)      // 延迟2秒来保证 JVM 存活
}

你可以点击这里获得完整代码

这里的 runBlocking<Unit> { …… } 作为一个适配器被用来启动最高优先级的主协程。 我们显式的声明 Unit 为返回值类型,因为Kotlin中的 main 函数返回 Unit 类型。

这也是一种使用挂起函数来实现单元测试的方法:

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // 这里我们可以使用挂起函数来实现任何我们喜欢的断言风格
    }
}

等待一个任务

延迟一段时间来等待另一个协程开始工作并不是一个好的选择。让我们显式地等待(使用非阻塞的方法)一个后台 Job 执行结束:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = GlobalScope.launch { // 启动一个新的协程并保持对这个任务的引用
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // 等待直到子协程执行结束
//sampleEnd
}

你可以点击这里来获得完整代码

现在,结果仍然相同,但是主协程与后台任务的持续时间没有任何关系。这样写会更好。

结构化的并发

这里还有一些东西我们期望的写法被使用在协程的实践中。 当我们使用 GlobalScope.launch 时我们创建了一个最高优先级的协程。甚至,虽然它是轻量级的, 但是它在运行起来的时候仍然消耗了一些内存资源。甚至如果我们失去了一个对新创建的协程的引用, 它仍然会继续运行。如果一段代码在协程中挂起(举例来说,我们错误的延迟了太长时间),如果我们启动了太多的协程,是否会导致内存溢出? 如果我们手动引用所有的协程和 join 是非常容易出错的。

这有一个更好的解决办法。我们可以在你的代码中使用结构化并发。 用来代替在 GlobalScope 中启动协程,就像我们使用线程时那样(线程总是全局的), 我们可以在一个具体的作用域中启动协程并操作。

在我们的例子中,我们有一个被转换成使用 runBlocking 的协程构建器的 main 函数, 每一个协程构建器,包括 runBlocking, 在它代码块的作用域内添加一个CoroutineScope 实例。 在这个作用域内启动的协程不需要明确的调用 join,因为一个外围的协程(我们的例子中的 runBlocking)只有在它作用域内所有协程执行完毕后才会结束。从而,我们可以使我们的示例更简单:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // 在 runBlocking 作用域中启动一个新协程
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

你可以点击这里来获取完整代码

作用域构建器

除了由上面多种构建器提供的协程作用域,也是可以使用 coroutineScope 构建器来声明你自己的作用域的。它启动了一个新的协程作用域并且在所有子协程执行结束后并没有执行完毕。runBlockingcoroutineScope 主要的不同之处在于后者在等待所有的子协程执行完毕时并没有使当前线程阻塞。

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // 创建一个新的协程作用域
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // 该行将在嵌套启动之前执行打印
    }
    
    println("Coroutine scope is over") // 该行将在嵌套结束之后才会被打印
}

你可以点击这里来获得完整代码

提取函数重构

让我们在 launch { …… } 中提取代码块并分离到另一个函数中。当你在这段代码上展示“提取函数”函数的时候,你得到了一个新的函数并用 suspend 修饰。 这是你的第一个 挂起函数 。挂起函数可以像一个普通的函数一样使用内部协程,但是它们拥有一些额外的特性,反过来说, 使用其它的挂起函数,比如这个例子中的 delay,可以使协程暂停执行。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

// 你的第一个挂起函数
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

你可以点击这里来获得完整代码

但是如果提取函数包含了一个调用当前作用域的协程构建器? 在这个例子中仅仅使用 suspend 来修饰提取出来的函数是不够的。在 CoroutineScope 调用 doWorld 方法是一种解决方案,但它并非总是适用,因为它不会使API看起来更清晰。 惯用的解决方法是使 CoroutineScope 在一个类中作为一个属性并包含一个目标函数, 或者使它外部的类实现 CoroutineScope 接口。 作为最后的手段,CoroutineScope(coroutineContext) 也是可以使用的,但是这样的结构是不安全的, 因为你将无法在这个作用域内控制方法的执行。只有私有的API可以使用这样的写法。

协程是轻量级的

运行下面的代码:

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // 启动大量的协程
        launch {
            delay(1000L)
            print(".")
        }
    }
}

你可以点击这里来获得完整代码

它启动了100,000个协程,并且每秒钟每个协程打印一个点。 现在,尝试使用线程来这么做。将会发生什么?(大多数情况下你的代码将会抛出内存溢出错误)

像守护线程一样的全局协程

下面的代码在 GlobalScope 中启动了一个长时间运行的协程,它在1秒内打印了“I'm sleeping”两次并且延迟一段时间后在main函数中返回:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 在延迟之后结束程序
//sampleEnd
}

你可以点击这里来获取完整代码

你可以运行这个程序并在命令行中看到它打印出了如下三行:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

GlobalScope 中启动的活动中的协程就像守护线程一样,不能使它们所在的进程保活。