Kotlin 协程
协程(Coroutine)与线程(Thread)
协程和线程的区别
协程和线程的共同目的之一是实现系统资源的上下文调用,不过它们的实现层级不同;
线程(Thraed)是比进程小一级的的运行单位,多线程实现系统资源上下文调用,是编程语言交付系统内核来进行的(可能是并发,也可能是伪并发),大部分的编程语言的多线程实现都是抢占式的,而对于这些线程的控制,编程语言无法直接控制,需要通过系统内核来进行,由系统内核决定最终的行为;
协程(Coroutine)是在语言层面实现“多线程”这个过程,一般在代码中以串行的方式表达并发逻辑,由于是在编程语言层面模拟这一过程,而非涉及到硬件层面,在编程语言层面可以完全控制这一过程,可以这么说,协程是软件层面模拟硬件层面的多线程;但不是说协程就一定是单线程,具体的实现要看具体编程语言的实现,kotlin的协程实现可能是单线程,也可能是多线程;
协程的使用场景
协程可以用于解决高负荷网络 IO、文件 IO、CPU/GPU 密集型任务等;
比如在IO线程高负载的场景下,CPU资源会被大量线程占用,这会极大浪费CPU资源,同时可能导致一些重要的线程被阻塞,代价是十分昂贵的,此时如果使用IO协程代替IO线程,可以大大减少线程数量,节省CPU资源,同时在协程挂起是几乎无代价的(不需要上下文切换或OS干预),同时编程语言对协程又有极大控制性;
Kotlin 1.1 中使用协程
Kotlin 1.1 开始提供了对于协程的支持,不过目前 kotlin 1.1 版本的协程处于实验性阶段,相关的包不包含在标准库中,需要手动导入"kotlinx-coroutines-core"包到项目依赖,可以直接 maven 中央仓库找到最新的 jar 包;
如果是使用 gradle 构建项目,在 build.gradle 脚本中需要添加类似以下:
apply plugin: 'java'
apply plugin: 'kotlin'
......
dependencies {
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.19.1' //添加相关包依赖
......
}
kotlin{ //开始协程实验性功能
experimental{
coroutines 'enable'
}
}
kotlin 对于协程提供了底层的API,同时提供了高级API方便使用,这里只介绍部分高级API的基本使用;
一个基本协程例子
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
fun main(args: Array) {
async(CommonPool) { //创建一个协程
delay(1000L) //协程挂起
log("World")
}
log("Hello,")
Thread.sleep(2000L) //主线程等待
}
//输出简要日志,以下例子使用了这个函数作简要日志输出,不再重复解释
fun log(message:String){ println( "[${Thread.currentThread().name}] $message") }
输出:
[main] Hello,
[ForkJoinPool.commonPool-worker-1] World
协程的创建
kotlin 中可以通过 async() 函数创建一个协程,该协程创建后立即运行,一个 async 中可以包含一个 使用"suspend" 关键字声明的挂起函数,或使用一个 lambda 表达式表示的匿名方法,如下:
import kotlinx.coroutines.experimental.*;
//async 使用suspend函数创建协程
fun main(args: Array) {
async(CommonPool) { //协程创建
foo()
}
log("Hello,")
Thread.sleep(2000L)
}
suspend fun foo(){ //挂起函数
delay(1000L)
log("World")
}
//async 使用匿名函数创建协程
fun main(args: Array) {
async(CommonPool) { //协程创建
delay(1000L)
log("World")
}
log("Hello,")
Thread.sleep(2000L)
}
在主线程上创建协程
以上的示例都是在 CommonPool 公共线程池上创建协程的,在main主线程中创建协程,可以如下标记 main:
fun main(args: Array) = runBlocking{
async(CommonPool) {
delay(1000L)
log("World")
}
log("Hello,")
delay(2000L) //此时main主线程也为协程,可以使用其他协程行为函数
}
协程的行为控制
kotlin 1.1 对于协程提供了以下常用的控制方法:
dely(millis:Long):协程挂起
Deffered.join():类似Thread.join
Deffered.cancel():协程取消,可携带一个 Exception 实例作为参数,以在协程取消时抛出该异常,可以通过返回值判断协程是否取消成功
Deffered.await():等待直接获取协程返回值
/*演示join()
在调用job.join时,main协程会挂起,直到job执行结束*/
fun main(args: Array) = runBlocking {
val job = async(CommonPool) {
delay(1000L)
log("World!")
}
log("Hello,")
job.join()
}
/*output:
[main] Hello,
[ForkJoinPool.commonPool-worker-1] World! */
/*演示cancel() */
fun main(args: Array) = runBlocking {
val job = async(CommonPool) {
delay(1000L)
log("World!")
}
log("Hello,")
job.cancel()
}
/*output:
[main] Hello, */
/*演示await()
以下示例演示了一个典型的异步过程,在执行work()之前,异步执行前置任务preWork1(),preWork2(),等待这两个
任务完全执行结束后,再执行word();
如果使用多线程来编写,需要些大量的回调函数,协程提供了一种简洁的编写方式*/
fun main(args: Array) = runBlocking {
val job1 = async(CommonPool) { preWork1() }
val job2 = async(CommonPool) { preWork2() }
work(job1.await(),job2.await())
}
suspend fun preWork1():String{
delay(3000L) //模拟一个耗时任务
return "job1:van ♂ yang"
}
suspend fun preWork2():String{
delay(1000L) //模拟一个耗时任务
return "job2:dark fantastic!"
}
fun work(str1:String,str2:String){
println("$str1\n$str2")
}
/*output:
job1:van ♂ yang
job2:dark fantastic! */
延迟生成器
kotlin 协程还提供了类似 Python 的生成器协程函数 buildSequence,用于执行一个延迟计算队列;
一个典型的应用就是生成斐波那契数列,一般编写会用递归的方式来编写,但是假如递归层级过大,很容易造成栈溢出,同时假如需要多次不等值地调用,会造成同一个值被多次计算,比较浪费资源,buildSequence 采用协程的方式,在每次调用时进行增量计算,避免同一值的多次调用,示例如下:
fun main(args: Array) {
//斐波那契数列
val fibonacci = buildSequence {
yield(1) //返回第一个斐波那契数
var cur = 1
var next = 1
while (true) {
yield(next) // 返回下一个斐波那契数
val tmp = cur + next
cur = next
next = tmp
}
}
//取出>100的斐波那契数
for (i in fibonacci){
println(i)
if(i > 100) break
}
//取出>500的斐波那契数,此时调用时 1-100 的斐波那契数是已经被延迟队列计算好的了
for (i in fibonacci){
println(i)
if(i > 500) break
}
}
协程和线程的对比示例
上面讲过协程很适合使用在高负荷IO、CPU密集型的场景,通过协程模拟并发的方式减少实际的线程数,已达到节省CPU资源的目的,以下2个示例分别创建 100,000 个线程和 100,000个协程模拟一个高负荷IO的场景:
创建 100,000 个线程
fun main(args: Array) = runBlocking {
val jobs = List(100_000) {
thread {
Thread.sleep(2000L) //模拟IO阻塞
log("hello")
}
}
jobs.forEach(Thread::join)
}
此时我的机器上创建了几千个线程后抛出栈溢出异常(-Xmx1024),同时CPU占满(i7-6700HQ)
创建 100,000 个协程
fun main(args: Array) = runBlocking {
val jobs = List(100_000) {
launch(CommonPool) {
delay(2000L) //模拟IO阻塞
print("hello")
}
}
jobs.forEach { it.join() } //这里不能用 jobs.forEach(Job::join),因为 Job.join 是 suspend 方法
}
程序运行时只创建了不到10个线程,并没有栈溢出,相比之下,在这样的场景里更适合使用协程;