协程
1协程是什么,kotlin官方文档说:本质上,协程是轻量级线程。如果将程序分为IO密集型应用和CPU密集型应用, 二者的发展历程大致如下:IO密集型应用: 多进程->多线程->事件驱动->协程;CPU密集型应用:多进程-->多线程;
如果说多进程对于多CPU, 多线程对应多核CPU, 那么事件驱动和协程则是在充分挖掘不断提高性能的单核CPU的潜力。常见的有性能瓶颈的API (例如网络 IO、 文件 IO、 CPU 或 GPU 密集型任务等), 要求调用者阻塞(blocking) 直到它们完成才能进行下一步。 后来, 我们又使用异步回调的方式来实现非阻塞, 但是异步回调代码写起来并不简单。协程提供了一种避免阻塞线程并用更简单,更可控的操作替代线程阻塞的方法:协程挂起;协程主要是让原来要使用“异步+回调方式” 写出来的复杂代码, 简化成可以用看似同步的方式写出来(对线程的操作进一步抽象) 。 这样我们就可以按串行的思维模型去组织原本分散在不同上下文中的代码逻辑, 而不需要去处理复杂的状态同步问题。简单看个栗子:
fun firstCoroutineDemo0() {
println("World")
Thread.sleep(5000L)
GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
}
输出
World
Hello,
World!
Builder.common.kt
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = new CoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
launch函数有3个入参:context、 start、 block, context 协程上下文; start 协程启动选项; block 协程真正要执行的代码块,必须是suspend修饰的挂起函数,这个launch函数返回一个Job类型, Job是协程创建的后台任务的概念, 它持有该协程的引用。 Job接口实际上继承自CoroutineContext类型。launch函数它以非阻塞(non-blocking) 当前线程的方式, 启动一个新的协程后台任
务, 并返回一个Job类型的对象作为当前协程的引用。这里的delay()函数类似Thread.sleep()的功能, 但更好的是:它不会阻塞线程, 而只是挂起协程本身。 当协程在等待时, 线程将返回到池中, 当等待完成时, 协同将在池中的空闲线程上恢复。
2 CommonPool: 共享线程池
首先, 这个CommonPool是代表共享线程池, 它的主要作用是来调度计算密集型任务的协程的执行。它的实现使用的是java.util.concurrent包下面的API。 它首先尝试创建一个 java.util.concurrent.ForkJoinPool (ForkJoinPool是一个可以执行ForkJoinTask的ExcuteService, 它采用了work-stealing模式:所有在池中的线程尝试去执行其他线程创建的子任务, 这样很少有线程处于空闲状态, 更加高效) ;如果不可用, 就使 用 java.util.concurrent.Executors 来创建一个普通的线程池: Executors.newFixedThreadPool 。
3 挂起函数
代码块中的 delay(3000L, TimeUnit.MILLISECONDS) 函数, 是一个用suspend关键字修饰的函数, 我们称之为挂起函数。 挂起函数只能从协程代码内部调用, 普通的非协程的代码不能调用。
挂起函数只允许由协程或者另外一个挂起函数里面调用, 例如我们在协程代码中调用一个挂起函数, 代码示例如下:
suspend fun runCoroutineDemo() {
run(CommonPool) {
delay(3000L, TimeUnit.MILLISECONDS)
println("suspend,")
}
println("runCoroutineDemo!")
Thread.sleep(5000L)
}
fun callSuspendFun() {
launch(CommonPool) {
runCoroutineDemo()
}
}
如果我们用Java中的Thread类来写类似功能的代码, 上面的代码可以写成这样:
fun threadDemo0() {
Thread({
Thread.sleep(3000L)
println("Hello,")
}).start()
println("World!")
Thread.sleep(5000L)
}
输出结果也是:
World! Hello,
注意我们不能使用Thread来启动协程代码。 例如下面的写法编译器会报错:
/**
* 错误反例: 用线程调用协程 error
*/
fun threadCoroutineDemo() {
Thread({
delay(3000L, TimeUnit.MILLISECONDS) // error, Suspend functions are only allowed to be //called from a coroutine or another suspend function
println("Hello,")
})
println("World!")
Thread.sleep(5000L)
}
使用纯的Kotlin的协程代码来实现上面的 阻塞+非阻塞 的例子(不用Thread),Kotlin中提供了runBlocking函数来实现类似主协程的功能:
fun main(args: Array<String>) = runBlocking<Unit> {
// 主协程
println("${format(Date())}: T0")
// 启动主协程
launch(CommonPool) {
//在common thread pool中创建协程
println("${format(Date())}: T1")
delay(3000L)
println("${format(Date())}: T2 Hello,")
}
println("${format(Date())}: T3 World!") // 当子协程被delay, 主协程仍然继续运行
delay(5000L)
println("${format(Date())}: T4")
}
运行结果:
14:37:59.640: T0
14:37:59.721: T1
14:37:59.721: T3 World!
14:38:02.763: T2 Hello,
14:38:04.738: T4
可以发现, 运行结果跟之前的是一样的, 但是我们没有使用Thread.sleep, 我们只使用了非阻塞的delay函数。 如果main函数不加 = runBlocking<Unit> , 那么我们是不能在main函数体内调用delay(5000L)的。runBlocking函数不是用来当做普通协程函数使用的, 它的设计主要是用来桥接普通阻塞代码和挂起风格的(suspending style) 的非阻塞代码的
等待一个任务执行完毕
fun firstCoroutineDemo() {
launch(CommonPool) {
delay(3000L, TimeUnit.MILLISECONDS)
println("[firstCoroutineDemo] Hello, 1")
}
launch(CommonPool, CoroutineStart.DEFAULT, {
delay(3000L, TimeUnit.MILLISECONDS)
println("[firstCoroutineDemo] Hello, 2")
})
println("[firstCoroutineDemo] World!")
}
运行这段代码, 我们会发现只输出:
[firstCoroutineDemo] World!
fun testYield() = runBlocking<Unit> {
launch() {
delay(3000L)
println("[firstCoroutineDemo] Hello, 1")
}
launch() {
delay(3000L)
println("[firstCoroutineDemo] Hello, 2")
}
println("[firstCoroutineDemo] World!")
}
输出:
[firstCoroutineDemo] World!
[firstCoroutineDemo] Hello, 1
[firstCoroutineDemo] Hello, 2
这是为什么?
为了弄清上面的代码执行的内部过程, 我们打印一些日志看下:
fun main() = runBlocking<Unit> {
val c1 = GlobalScope.launch() {
println("C1")
println("C1 Starat")
delay(3000L)
println("C1 world! 1")
}
val c2 = launch() {
println("C2")
println("C2 Start")
delay(3000L)
println("C2 world! 2")
}
println("Main Thread : ${Thread.currentThread()}")
println("Hello,")
println("Hi,")
println("C1 is activie:${c1.isActive} ${c1.isCompleted}")
println("C2 is activie:${c2.isActive} ${c2.isCompleted}")
}
输出:
C1
C1 Starat
Main Thread : Thread[main,5,main]
Hello,
Hi,
C1 is activie:true false
C2 is activie:true false
C2
C2 Starat
C1 world! 1
C2 world! 2
这里的C1、 C2代码也开始执行了, 使用的是 ForkJoinPool.commonPool-worker 线程池中的worker线程。 但是, 我们在代码执行到最后打印出这两个协程的状态isCompleted都是false, 这表明我们的C1、 C2的代码, 在Main Thread结束的时刻(此时的运行main函数的Java进程也退出了) , 还没有执行完毕, 然后就跟着主线程一起退出结束了。
所以我们可以得出结论:运行 main () 函数的主线程, 必须要等到我们的协程完成之前结束 , 否则我们的程序在 打印Hello, 1和Hello, 2之前就直接结束掉了。我们怎样让这两个协程参与到主线程的时间顺序里呢?我们可以使用 join , 让主线程一直等到当前协程执行完毕再结束, 例如下面的这段代码
fun main() = runBlocking<Unit> {
val c1 = GlobalScope.launch() {
println("C1")
println("C1 Starat")
delay(3000L)
println("C1 world! 1")
}
val c2 = launch() {
println("C2")
println("C2 Start")
delay(3000L)
println("C2 world! 2")
}
println("Main Thread : ${Thread.currentThread()}")
println("Hello,")
println("C1 is activie:${c1.isActive} ${c1.isCompleted}")
println("C2 is activie:${c2.isActive} ${c2.isCompleted}")
c1.join() // the main thread will wait until child coroutine completes
println("Hi,")
println("C1 is End activie:${c1.isActive} ${c1.isCompleted}")
println("C2 is End activie:${c2.isActive} ${c2.isCompleted}")
c2.join()
println("C1 is End End activie:${c1.isActive} ${c1.isCompleted}")
println("C2 is End End activie:${c2.isActive} ${c2.isCompleted}")
}
C1
C1 Starat
Main Thread : Thread[main,5,main]
Hello,
C1 is activie:true false
C2 is activie:true false
C2
C2 Start
C1 world! 1
Hi,
C1 is End activie:false true
C2 is End activie:true false
C2 world! 2
C1 is End End activie:false true
C2 is End End activie:false true