Kotlin 协程的使用

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个线程,并没有栈溢出,相比之下,在这样的场景里更适合使用协程;













你可能感兴趣的:(Kotlin,Kotlin,开发小结)