Kotlin语言中协程(Coroutines)的使用

写在前面

  • 什么是协程(coroutines)
    协程是一种类似于轻量级,更高效的线程(实际上它不是线程).为什么说它轻量级且高效呢,因为它实际上还是在当前线程中操作,但是它执行任务时又不会阻塞当前线程,所以它没有切换线程带来的额外资源消耗,实际开发中你你能开启的线程数量是有限的,并且线程是由操作系统控制的.但协程只要你的CPU和内存资源足够,你完全可以开启100000个协程,并且每个协程都由你自己来操作,你可以决定何时关闭或者开启协程.
    协程并不是kotlin语言发明和创造的,在C#,python,go等语言都有协程,C++也可以实现协程(微信后台服务器的C++代码就使用了微信自己开发的协程库,因为使用协程替代了多线程,并发能力得到百倍的提升)

  • 为何要使用协程
    更高效,上面已经解释了,协程比线程更高效,并且可控性更强,而且更为安全,因为实际都在同一个线程中运行,不会存在线程安全性问题.最最重要的是,协程写起来更加简单逻辑理解起来也更加容易,不需要常规的异步操作那样不断地回调,最后进入回调地狱(callback hell)
    在Android开发中,google提供了Android对kotlin协程的支持库,并且在IO开发者大会上强烈推荐使用kotlin的coroutines进行IO操作,在Android中如何使用kotlin协程请求网络或者读取数据库等异步操作,我在后面的博客中会介绍,今天主要看看协程的一些基本概念和使用

如何使用协程

首先保证你的kotlin版本是1.3以上,1.3以上coroutines存在于标准库中了
通过代码来看看coroutines具体怎么使用

  • 配置环境
    使用intellij idea 2018.3.3(顺便说一句,作为一个Android开发者,最好也要装一个idea,idea不仅具备Android studio所有功能,并且还能开发后端,还能开发前端,开发gradle,有助于理解前后端以及gradle编译原理等),新建一个project,选择gradle项目,右边选择kotlin(Java),后面根据需要填写,一路next直至创建完成
    打开项目根目录的build.gradle文件

    plugins {
        id 'org.jetbrains.kotlin.jvm' version '1.3.11'
    }	
    

    确保version在1.3以上
    在dependencies 中添加以下这句(标准的kotlin中只含有suspend关键字,不包含launch,async/await 等功能,所以需要添加以下支持库)

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
    

    如果是在Android项目中使用,还需要添加以下

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
    
  • suspend函数和runblocking
    新建一个src目录下新建一个kotlin文件main.kt
    首先我们来看一下做一个普通的延迟打印,用线程的方式是怎么做的

    fun main(args: Array<String>) {
       	 exampleBlocking()
    }
    /**
     * 打印信息和当前时间戳的最后四位
     */
    fun printlnWithTime(message: String){
        println("$message -- ${System.currentTimeMillis().toString().takeLast(4)}")
    }
    
    fun printlnDelayed(message: String) {
    	//当前线程延迟1000毫秒,这会阻塞当前线程
        Thread.sleep(1000)
        printlnWithTime(message)
    }
    
    fun exampleBlocking() {
        printlnWithTime("one")
        printlnDelayed("two")
        printlnWithTime("three")
    }
    

上述代码运行的结果是

one -- 4629
two -- 5649
three -- 5649

Process finished with exit code 0

第二次打印和第一次打印延迟1000毫秒

再来看看用协程的方式怎么做的

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

fun main(args: Array<String>) {
    exampleBlocking()
}
/**
* 打印信息和当前时间戳的最后四位
*/
fun printlnWithTime(message: String){
	println("$message -- ${System.currentTimeMillis().toString().takeLast(4)}")
}

//suspend表示当前函数为挂起函数,只能在CoroutineScope或者另一个suspend函数中调用
suspend fun printlnDelayed(message: String) {
	//表示延迟1000毫秒,delay同样是suspend函数
    delay(1000)
    printlnWithTime(message)
}
//runBlocking 后面的闭包就是执行在CoroutineScope中,所以可以在其中直接调用suspend函数
fun exampleBlocking() = runBlocking {
    printlnWithTime("one")
    printlnDelayed("two")
    printlnWithTime("three")
}

上述代码执行的结果是:

one -- 1946
two -- 2961
three -- 2962

Process finished with exit code 0

结果一致,第二次打印和第一次打印间隔1秒

上面的代码中delay()为suspend函数(挂起函数),suspend函数只能在CoroutineScope(协程作用域)中,或者另外一个suspend函数中调用,所以我们定义printlnDelayed()函数也为suspend函数.
runblocking会启动一个协程并且阻塞当前线程一直到其中所有的协程都执行完毕.
runblocking后面的闭包就是在CoroutineScope中,所以可以在其中直接调用suspend函数.

  • CoroutineDispatcher 协程调度器

将上述代码中exampleBlocking()方法改成以下方法

fun exampleBlockingDispatcher() {
    runBlocking(Dispatchers.Default) {
        printlnWithTime("one - from thread ${Thread.currentThread().name}")
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
}

执行结果如下

one - from thread DefaultDispatcher-worker-1 -- 2970
two - from thread DefaultDispatcher-worker-1 -- 3985
three - from thread main -- 3987

Process finished with exit code 0

one和two之间仍然是间隔1000毫秒执行,但是,one,two在worker-1线程中执行,而three仍然在main线程执行.并且three也是在runblocking全部执行完了才执行,也正好印证了我们刚才说的runblocking会阻塞当前线程(即使是在另一个线程执行协程).
通过源码我们知道,runblocking有两个参数,第一个参数是CoroutineContext(协程上下文)是可选参数,第二个参数就是后面大括号的方法.Dispatchers.Default的类型是CoroutineDispatcher(协程调度器)就是CoroutineContext的子类,协程调度器可以将协程的执行局限在指定的线程中,这在Android开发中很有用,后面的博客会详述.

  • launch函数
    将上面的方法exampleBlockingDispatcher()改成如下代码
fun exampleLaunch() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    //launch表示启动一个新的协程,但不阻塞当前线程
    launch {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
}

执行结果如下:

one - from thread main -- 9551
three - from thread main -- 9563

Process finished with exit code 0

你可能会奇怪了,two跑哪里去了,怎么没打印two,是launch里面的方法没执行吗?其实不是,程序先执行了one,然后执行了launch.launch表示启动一个新的协程并且不阻塞当前线程,launch后面的闭包是执行在在CoroutineScope中,但请注意launch本身和runblocking一样不是suspend函数.launch启动一个协程可能需要几毫秒,但是程序不会暂停,程序会接着执行three,当执行完了three,runblocking会判断,我闭包中的所有代码都执行了(因为launch函数也执行了,尽管launch完全执行所有代码需要时间,但是launch不是suspend函数,runblocking不会等待非suspend函数),我该结束了,于是,它结束了程序,而launch闭包中的延时还没来得及执行,程序就被关闭了,所以two没有被打印出来.
那么我们想让two打印出来呢,我们只需要延迟一下程序关闭,让launch有时间执行完所有代码

fun exampleLaunch() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    launch {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
    //延迟以等待launch中的代码执行完毕
    delay(3000)
}

执行结果如下:

one - from thread main -- 7919
three - from thread main -- 7931
two - from thread main -- 8940

Process finished with exit code 0

这样two在大约1秒之后被打印了
launch也可以和runblocking一样配置协程调度器,当调用 launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下文(以及调度器)。在这个案例中,它从 main 线程中的 runBlocking 主协程承袭了上下文。如果我配置了launch的调度器,它会从指定的线程中执行,代码如下:

fun exampleLaunch() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    launch(Dispatchers.Default) {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
    delay(3000)
}

执行结果如下:

one - from thread main -- 8182
three - from thread main -- 8195
two - from thread DefaultDispatcher-worker-2 -- 9201

Process finished with exit code 0

以上two在另外一个线程中执行了.
请注意,launch函数是CoroutineScope的方法,如果要调用launch必须要有CoroutineScope对象,上面代码的launch{…} 其实是 this.launch{…},而这个this是runblocking闭包中的CoroutineScope对象
如果没有CoroutineScope对象,可以使用GlobalScope.launch{…},GlobalScope是一个全局的CoroutineScope对象

  • 使用Job控制协程

有没有发现runblocking最后一行的delay实在太low了,万一我launch中的代码执行时间不确定怎么办,那我怎么知道要delay多久.那我如何控制我的协程呢,将上面的exampleLaunch()方法改成如下代码:

fun exampleLaunchWait() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    //launch会返回一个Job对象,该对象代表新启动的协程,可以通过job来操作协程
    val job = launch {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
    //join()是suspend函数,它的作用是挂起当前协程,直到job完成
    job.join()
}

执行结果如下

one - from thread main -- 7976
three - from thread main -- 7989
two - from thread main -- 8999

Process finished with exit code 0

Job在Android 开发中很有用,比如如果你要在Activity结束时关闭异步任务,可以在onDestory()中调用job.cancel() ,虽然实际开发中为了架构的解耦性和可测试性,我们一般不会在Activity或者Fragment中直接处理数据,但是job.cancel()仍然可以使用在ViewModel中,关于这一块,后面的博客会详解.

  • async和withContext
    async和launch类似,都是创建一个新的协程(请注意,launch创建一个新的协程之后会立即启动,默认情况下,async也会立即启动,但也可以设置惰性启动),不同的是.launch{…}返回Job,async{…}返回Deferred,Deferred的await()方法可以立即获得协程的结果.先看代码:
import kotlinx.coroutines.*
fun main(args: Array<String>) {
    exampleAsyncAwait()
}
/**
 * 模拟复杂计算
 */
suspend fun calculateHardThings(startNum: Int): Int {
    delay(1000)
    return startNum * 10
}

fun exampleAsyncAwait() = runBlocking {
    val startTime = System.currentTimeMillis()
    //启动一个新的协程,用于计算结果
    val deferred1 = async { calculateHardThings(10) }
    val deferred2 = async { calculateHardThings(20) }
    val deferred3 = async { calculateHardThings(30) }
    //await会阻塞当前线程,等待计算完毕,并且返回协程的计算结果
    val sum = deferred1.await() + deferred2.await() + deferred3.await()
    println("sum = $sum")
    val endTime = System.currentTimeMillis()
    println("总耗时: ${endTime - startTime}")
}

执行结果如下:

sum = 600
总耗时: 1016

Process finished with exit code 0

async不会阻塞线程,但是await会阻塞线程,因为是启动了三个协程用于分别计算,所以await的时间也就是1秒左右,对比一下以下代码

//反面例子
fun exampleAsyncAwait() = runBlocking {
    val startTime = System.currentTimeMillis()
    //await将会阻塞当前线程,直至async中协程执行完毕了才会放行
    val deferred1 = async { calculateHardThings(10) }.await()
    val deferred2 = async { calculateHardThings(20) }.await()
    val deferred3 = async { calculateHardThings(30) }.await()
    val sum = deferred1 + deferred2 + deferred3
    println("sum = $sum")
    val endTime = System.currentTimeMillis()
    println("总耗时: ${endTime - startTime}")
}

执行结果:

sum = 600
总耗时: 3025

Process finished with exit code 0

await将会阻塞当前线程,直至async中协程执行完毕了才会放行,相当于计算一个接着才能计算下一个,所以总耗时3秒,这当然是不好的,这是反面例子.
async适用于需要多次调用并且需要知道结果的场景,比如多任务下载.如果我不需要多次调用只需要知道结果,还可以使用withContext,withContext的效果类似于上面代码的async{…}.await(),不同的是withContext没有默认调度器必须要指定一个协程调度器,代码如下:

fun exampleWithContext() = runBlocking {
    val startTime = System.currentTimeMillis()
    val result1 = withContext(Dispatchers.Default) { calculateHardThings(10) }
    val result2 = withContext(Dispatchers.Default) { calculateHardThings(20) }
    val result3 = withContext(Dispatchers.Default) { calculateHardThings(30) }
    val sum = result1 + result2 + result3
    println("sum = $sum")
    val endTime = System.currentTimeMillis()
    println("总耗时: ${endTime - startTime}")
}

执行结果:

sum = 600
总耗时: 3020

Process finished with exit code 0

withContext很适合需要直接获取协程结果但又不会短时间内重复调用的场景,比如网络请求.

好了,基本上常用的协程使用方法都介绍了,当然协程还有更多的使用方式方法,本文只是抛砖引玉
最后,还记得当年学习线程的时候卖火车票的例子吗,我们用协程也来实现一个,如果你能独立实现,说明你已经理解了协程的用法了,代码如下:

import kotlinx.coroutines.*
import kotlin.random.Random

fun main(args: Array<String>) {
    saleTickets()
}

fun saleTickets() = runBlocking {
    //要卖出的票总数
    var ticketsCount = 100
    //售票员数量
    val salerCount = 4
    //此列表保存async返回值状态,用于控制协程等待
    val salers: MutableList<Deferred<Unit>> = mutableListOf()
    repeat(salerCount) {
        val deferred = async {
            while (ticketsCount > 0) {
                println("第${it + 1}个售票员 卖出第${100 - ticketsCount + 1}张火车票")
                ticketsCount--
                //随机延迟100-1000毫秒,使每次售票时间不相同
                val random = Random.nextInt(10)+1
                delay((random * 100).toLong())
            }
        }
        salers.add(deferred)
    }
    salers.forEach { it.await() }
}

转载请注明出处

你可能感兴趣的:(kotlin)