本文翻译来自kotlin官方文档
https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html
kotlin,最为一门编程语言,在它的标准类库中只提供了最低级别AP来支持其他类库使用协程。
不像其它有类似协程能力编程语言那样,async(异步)和await(等待)在kotlin并不是关键字,它们甚至不是kotlin标准类库的一部门。
此外,在kotlin的概念中,终端函数为异步操作 提供了一种更安全,更少错误的抽象。
JetBrains为kotlin的协程开发了一个丰富的类库kotlinx.coroutines,它包含了一系列的高级的协程机制,其中就包括了launch,async和其它机制。
本文提供了一系列的例子还介绍协程(kotlinx.coroutines),它分成了不同的主题。
为了使用好协程,同时也为了跟上这些例子,首先你需要添加协程core类的依赖。具体地址为:
https://github.com/kotlin/kotlinx.coroutines/blob/master/README.md#using-in-your-projects
如果你只想先了解或者熟悉一下coroutines,使用如下配置即可:
buildscript {
ext.kotlin_version = '1.3.41'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
dependencies {
//...other implementation....
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// coroutines-core
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC'
// coroutines-android
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-RC'
}
下面就是你的第一行协程代码:
import kotlinx.coroutines.*
fun main() {
//后台启动一个新的协程
GlobalScope.launch {
// 协程非阻塞延迟1s
delay(1000L)
println("hello world")
}
println("hi")
//主线程等待3s,同时协程在等待2s
Thread.sleep(3000L)
}
结果为:
从本质上来讲,协程(coroutines)只是轻量级的线程。它们在CoroutineScope上下文中通过launch协程builder启动。这里通过GlobalScope方式可以启动一个新的协程(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html) ,那么意味着新启动协程的生命周期只会受限于整个应用的生命周期(换句话说使用GlobalScope启动的协程就像Application的生命周期一样,伴随的应用而开始,随着应用的死亡而消失)。
实现以上代码类似的结果,你可以使用GlobalScope.launch { … } 和 thread { … } 或者使用 delay { … }和 Thread.sleep { … } 替代,你可以尝试一下。
但是如果你如果使用thread来替代 GlobalScope.launch,编译器就会报如下错误:
Suspend functions are only allowed to be called from a coroutine or another suspend function
翻译过来就是:
使用Suspend 修饰的中断函数 只能在协程中 或者其他使用Supspend中断函数中 使用,在thread中不能使用。
这是因为deley是一种特殊的终端函数,它并不阻塞线程,但是会中断阻塞协程。所以它只能用在协程中。
在上面的例子中,我们展示了使用混合的非阻塞方式 delay(…) 和 阻塞方式 Thread.sleep (…) 的代码。但是这样的代码很容易失去控制,特别是在阻塞和非阻塞混合代码中。我们通过runBlocking协程构造器显式使用阻塞:
fun main() {
GlobalScope.launch {
delay(1000L)
println("hello world")
}
println("hi")
// 这里会阻塞主线程
runBlocking {
delay(3000L)
}
}
运行结果和上面是一致的,但是这里的代码只使用了非阻塞式delay。主线程通过调用 runBlocking来等待协程内部逻辑执行完成。
这个例子还可以写成更符合习惯的代码,使用runBloking包装执行main函数:
fun main() = runBlocking<Unit> {
GlobalScope.launch {
delay(1000L)
println("hello world")
}
println("hi")
delay(3000L)
}
这里的runBlocking { … } 作为一个适配器来启动一个顶级的主协程。我们明确的指定Unit作为返回值类型,因为在kotlin一个好的main函数需要一个返回值。
当然了,你也可以使用单元测试Junit来测试中断函数了。
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking {
// here we can use suspending functions using any assertion style that we like
}
}
使用delay多长时间来等待协程完成并不是一个好的方法(这个是显然的,上面的例子中我们有很多时间是浪费的),那我们就明确等待,等待的时长就是后台协程完成的时间。
fun main() = runBlocking {
val job = GlobalScope.launch {
println("job start....")
delay(2000)
println("job end....")
}
println("main thread...")
//等待协程完成,主线程即中断
job.join()
}
结果为:
主协程等到子协程完成之后,就退出程序,这个代码比上面的代码要好。()
但是实际过程使用协程还存在一些不足之处。当我们使用GlobalScope.launch时,我们创建了一个顶级的协程。
即使它是轻量级的,但它在运行时还是要消耗一些内存资源。如果我们忘记保留对新启动的协程持有引用,那么它将会持续运用。
如果代码在协程中挂起(例如,我们错误的延迟太长时间)我们该怎么办?如果我们启动了太多的协程,消耗完了内存我们该怎么办?手动添加对所有启动协程的引用,然后join它们(等待它们结束)是非常容易出错的。
这里有一个很好的解决办法。我们可以在代码中使用结构化并发。这次并不是使用GlobalScope启动协程,
就像我们平时使用线程那样(线程总是全局概念的),我们在指定的scope内启动协程。
在我们的例子中,我们使用runBlocking协程构建器将main函数转变成了一个协程函数。每一个协程构建器,包括runBlocking,都会添加一个CoroutineScope实例到这个Scope中。我们可以在这个Scope中启动协程,并不需要明确的使用join协程(等待协程结束),因为一个外部的协程(我们的例子是runBlocking)会等到内部的所有的协程完成之后才会完成。因为我们可以使我们的例子更为简单:
fun main() = runBlocking {
launch {
// 在runBlocking启动一个新的协程
delay(2000)
println("world")
}
println("hello")
}
(Scope我们可以理解为范围,类似于Java代码中全局属性/局部属性的含义)
除了了通过不同的协程构建器构建协程scope,你还可以通过coroutineScope来定义自己的scope。它能创建一个协程scope,会等到所有的子协程完成之后
自身才会完成。runBlocking和coroutineScope两种Scope最大的不同是,coroutineScope在等待子协程完成时,并不是阻塞当前线程。举个例子:
fun main() = runBlocking {
launch {
delay(2000L)
println("task from runBlocking")
}
coroutineScope{
launch {
delay(400L)
println("task from nested launch")
}
delay(100L)
//由于coroutineScope并不会阻塞当前线程,所以这个是最先打印的。
println("task from coroutine scope")
}
// 这里会等到 nested launch 完成之后 才会打印
// 所以它是阻塞式的,会等到所有的子协程完成之后 自身才会完成
println("Coroutine scope is over")
}
我们可以将launch{}
中的代码块抽取为一个单独的函数。当你执行抽取函数时,新的函数会有一个suspend
修饰符。这将会是你的第一个延迟函数
。延迟函数可以用在协程中,就像普通函数那样使用。但是它额外的功能就是它们能反过来调用其他的延迟函数,就像下面例子中的delay
一样,在协程中延迟执行功能。
fun main() = runBlocking {
launch {
doWorld()
}
print("main world.")
}
suspend fun doWorld() {
delay(1000)
print("hello world")
}
运行结果为:
但是,如果提取的函数包含在当前作用域上调用的协程构造器,那我们该怎么办呢?这种情况下只在抽取的函数前面加一个suspend
修饰符是不够的。其中的一个解决方式就是将doWorld
作为CoroutineScope
的一个扩展方法,但是它不是总是有效的,因为它会使API不清晰可见。而我们惯用的做法就是将显式的CoroutineScope
作为包含目标函数的类中的字段,或者在外部类中实现CoroutineScope
时使用隐式字符。最终的解决方案,可以使用CoroutineScope(coroutineContext) ,但是这种实现在结构上是不安全的,因为你将不会在scope中控制方法的执行,只有在私有的API中可以使用这种模式。
执行下面的代码:
fun main() = runBlocking {
repeat(100_000_0){
launch {
delay(1000L)
print(".")
}
}
}
这里将会打印一百万个点,但是如果你用线程来实现上面的需求时,大概会抛出OOM了,(所以协程的高性能就显而易见了。)
下面的代码中在GlobalScope
启动了一个长时间运行的协程,一秒中将会打印"I’m sleeping",主函数中延迟一段时间之后,全局函数将会退出。
fun main() = runBlocking {
GlobalScope.launch {
repeat(1000){
println("I'm sleeping $it ...")
delay(100)
}
}
delay(300)
println("hello world...")
}
打印结果为:
我们可以看到,只打印了三行。在GlobalScope
中激活的协程并不保证进程的活动状态,它们只像守护线程一样,等到主线程结束之后,就会主动结束。