kotlin coroutine文档:基础

协程基础

这部分讲述协程基础概念。

第一个协程

运行如下代码:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台启动新协程,然后继续运行
        delay(1000L) // 非阻塞式延时1秒(默认时间单位是毫秒)
        println("World!") // 在延时后打印
    }
    println("Hello,") // 当协程延时时主线程继续
    Thread.sleep(2000L) // 为了保活JVM,阻塞主线程两秒
}

这里获取完整代码

结果如下:

Hello,
World!

本质上,协程是轻量级线程。它们由launch 协程生成器(coroutine builder) 在某个CoroutineScope的环境下启动。 这里,我们在GlobalScope中启动了一个新的协程,意思是,新协程的生命周期仅仅由整个应用的生命周期限制。

用thread { … } 替代GlobalScope.launch { … }、delay(…) 替代 Thread.sleep(…),你可以达到相同的效果。快试试吧!

如果你thread替代GlobalScope.launch,编译器产生如下错误:

    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)  // ... 为了保活JVM,延迟了两秒
}

这里获取完整代码

结果是一样的,但是这个代码仅仅用了非阻塞式delay。在主线程,调用了runBlocking,阻塞直到runBlocking里面的协程结束。

扫描二维码关注公众号,回复: 4898525 查看本文章

这个例子也可以用更加惯用的方式重新编写,即使用runBlocking包裹主函数的执行:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // 开启主协程
    GlobalScope.launch { // 在后台启动新协程,然后继续运行
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主线程立即运行到这里
    delay(2000L)      // 为了保活JVM,延迟了两秒
}

这里获取完整代码

这里runBlocking<Unit> { … } 作为一个适配器使用,这个适配器用来开启顶层主协程。显式地指定它的Unit返回类型,这是因为在kotlin里面,一个符合语法规则的main函数不得不返回Unit。

如下也是为挂起函数编写单元测试的一种方式:

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // 通过使用断言样式,这里可以使用挂起函数
    }
}

等待一个job

在另外一个协程运行时延迟一段时间,这不是一个好方法。让我们显式地等待(以非阻塞式),直到我们启动的后台Job结束:

import kotlinx.coroutines.*

fun main() = runBlocking {
//例子开始
    val job = GlobalScope.launch { // 启动新协程,保留它的Job的引用
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // 等待直到子协程结束
//例子结束    
}

这里获取完整代码

这个结果仍旧是相同的,但是主协程的代码,不再以任何方式,绑定到后台job的时长,好得多了!

结构化的并发

在实际使用协程时,有一些东西还是要改进的。当使用GlobalScope.launch时,我们创建了一个顶层协程。即使它是轻量级的,但是它运行时仍旧会消耗内存资源。如果我们忘记了保留新启动协程的引用,而这个协程还在运行。协程中代码 (hang)挂住 了(例如,我们错误地延迟太长时间),该怎么办?我们启动了太多协程而耗尽了内存,该怎么办?为所有启动的协程不得不手动地保留引用,然后join它们,这很容易出错。

有个更好的解决方案。我们可以在代码中使用结构化的并发。我们不是在GlobalScope中启动协程,就像我们经常处理线程一样(线程一直是全局的),而是在我们执行操作的特定作用域里面启动协程。

在这个例子中,我们有main函数,它用runBlocking协程生成器变成一个协程。每个协程生成器,包括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") // 这一行会在嵌入的launch之前打印
    }
    
    println("Coroutine scope is over") // 这一行直至所有嵌入的launch结束时才打印
}

这里获取完整代码

提取函数的重构

让我们提取launch { … }里面的代码块到一个独立函数。当你对这个代码执行“提取函数”重构时,你会得到一个带有suspend修饰的新函数。这就是你第一个挂起函数(suspending function)。挂起函数可以在协程内使用,就像普通函数一样,但是它们的附加特性是,它们转而可以使用其他的挂起函数来挂起一个协程的执行,像这个例子中的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。万不得已,也可以使用CoroutineScope(coroutineContext) ,但是这个方法在结构上是不安全的,因为你不再可以控制这个方法执行的作用域了。仅仅在私有API可以使用这个生成器。

协程 轻量级的

运行如下代码:

import kotlinx.coroutines.*

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

这里获取完整代码

它启动了十万个协程,一秒后,每个协程打印了一个点。现在,用线程试试。会发生什么呢?(最可能的是,我们的代码会出现某种内存溢出的错误)

全局协程就像守护线程

如下代码在GlobalScope上启动了一个长期运行的协程,每一秒打印"I’m sleeping"两次,然后在某个延迟之后从主函数返回:

import kotlinx.coroutines.*

fun main() = runBlocking {
//例子开始
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟后退出
//例子结束    
}

这里获取完整代码

你可以运行和看到它打印了三行然后终止:

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

GlobalScope里启动的活跃协程没有让进程保活。它们就像守护线程。[注:GlobalScope不会绑定到任何job]

猜你喜欢

转载自blog.csdn.net/tigershin/article/details/86372480