下面这个博客对协程的讲解非常清楚
https://kaixue.io/kotlin-coroutines-1/
kotlin 官方中文资料
https://www.kotlincn.net/docs/reference/
协程是一套线程框架,是对线程中执行的代码顺序的管理,协程中的代码依然在线程中运行;协程设计的初衷是为了解决并发问题,让协作式多任务实现起来更加方便。协程是一种编程思想,在其他语言中也有实现如Go、Python、Java的Kilim等。
举个协程的栗子
launch{
val img = getImage(); // 耗时操作 需要等待
show(img); // UI 显示图像 应在主线程执行
}
{}中的代码就是一段协程代码,其中getImage() 方法在子线程中执行,而show方法在主线程中执行,以同步代码的方式实现了异步逻辑,这段代码中并未使用回调函数将获取到的图像传递给show方法,如果都使用这样的写法可以避免出现回调地狱场景。
协程创建方式 | 说明 | 使用范围 |
---|---|---|
runBlocking{...} | 创建新的协程,运行在当前线程上,所以会堵塞当前线程, 直到协程体结束;但是这个runBlocking域中可以有多个协程, 多个协程可以并发进行,不会等待子协程执行结束 |
用于启动一个协程任务,通常只用于启动最外层的协程, 例如线程环境切换到协程环境 |
GlobalSocpe.launch{...} | 启动一个新的线程,在新线程上创建运行协程, 不堵塞当前线程 |
需要启动异步线程处理的情况 |
CoroutineScope(Dispathcer.xxx).{...} | 在指定类型的线程中创建协程,不会阻塞所运行的线程 |
上面的launch方法可以的并列方法是async方法
lauch
:协程构建器,创建并启动(也可以延时启动)一个协程,返回一个Job,用于监督和取消任务,用于无返回值的场景。async
:协程构建器,和launch一样,区别是返回一个Job的子类 Deferred
,async可以在协程体中自定义返回值,并且通过Deferred.await堵塞当前线程等待接收async协程返回的类型。特别是需要启动异步线程处理并等待处理结果返回的场景CoroutineScope 创建新一个子域,并管理域中的所有协程。注意这个方法只有在block中创建的所有子协程全部执行完毕后,才会退出。
SuperVisorScope 在子协程失败时,错误不会往上传递给父域,所以不会影响子协程。
最常用的方法withContext(Dispatcher.xxx){code}
该方法让code代码运行在Dispatcher.xxx指定的线程中, 运行结束后再自动切回到调用withContext的线程。
suspend fun doSomething(){
withContext(Dispathcer.IO){
var img = getImage() // 耗时操作, getImag 是挂起函数
}
}
如在主函数中调用doSomething() 则线程先从主线程切到IO线程,等协程执行完之后再切换回主线程
上述切换协程的运行线程使用了协程的调度器,协程的调度器有如下几种
类型 | 作用 | 场景 |
---|---|---|
Dispatcher.Main | 使协程运行在主线程 | 更新UI |
Dispatcher.IO | 使协程运行在IO线程 | 用于网络请求和文件访问 |
Dispatcher.Default | 使用共享线程运行协程 | CPU密集型任务 |
Dispatcher.UnConfined | 使用父协程运行的线程 | 高级调度器,不应该在常规代码里使用 |
newSingleThreadContext | 在新线程中运行协程 |
抛物线的这篇文章对挂起的解释很清楚 https://kaixue.io/kotlin-coroutines-2/
挂起函数 关键字 suspend
suspend 关键字用来标识一个函数是挂起函数,但是suspend关键字标识的函数内部不一定有协程的挂起操作,如果函数使用了suspend关键字则函数只能在协程内或另一个挂起函数中被调用。
什么是挂起
aunch
,async
或者其他函数创建的协程,在执行到某一个 suspend
函数的时候,这个协程会被「suspend」,也就是被挂起。
那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。
注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。
suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。
协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程。
再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作。
当执行到一个挂起函数时,当前线程将不再执行这个挂起函数和该协程后续代码,直到这个挂起函数执行结束(或者这个挂起函数切回这个线程)。这个挂起函数仍然被执行,但可能在另一个线程中被继续执行,当然也可能继续在当前线程执行,根据代码中设置的调度器而定。
存在多个协程时,有些协程直接没有交互关系,而有些协程需要另一个协程执行完的结果,对于没有关系的协程可以让他们并行执行,对于有关系的协程可以让他们异步执行。
runBlocking {
Log.d("dd","zero")
Log.d("dd",Thread.currentThread().name)
GlobalScope.async(Dispatchers.Main) {
Log.d("dd", "one")
delay(200)
Log.d("dd", "two")
} // .await()
GlobalScope.async(Dispatchers.Main){
Log.d("dd", "three")
delay(100)
Log.d("dd", "four")
} //.await()
Log.d("dd","five")
}
Log.d("dd", "six")
one->three->four->two 协程是并发进行的
增加await()方法后的输出:
one->two->three->four 协程是按顺序执行的,第一个执行完再执行第二个
取消一个协程可以使用cancel()方法Job#cancel()、 Deferred#cancel()、cancelAndrJoin()方法
kotlin提供的挂起函数都是可以取消的,自定义的挂起函数如果想可以被取消可以在挂起函数中判断isActive状态,当调用cancel()方法后这个状态会发生变化。
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancel() // 取消该作业
job.join() // 等待作业执行结束
println("main: Now I can quit.")
输出结果如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
由于delay函数是coroutine的自带挂起函数,可取消所以运行到2就结束了。
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 可以被取消的计算循环
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并等待它结束
println("main: Now I can quit.")
运行结果同上,这个协程中执行了耗时操作,可以将其抽象成一个挂起函数,注意while中的判断条件isActive,用来结束协程。如果监听这个状态而调用cancel()协程不会被取消知道运行结束。
当一个父协程被取消的时候,所有它的子协程也会被递归的取消。然而,当使用 GlobalScope 来启动一个协程时,则新协程的作业没有父作业。 因此它与这个启动的作用域无关且独立运作。
携程的执行时间可以受到约束,我们使用withTimeout、withTimeoutOrNull
withTimeout(time)方法如果超时了会爆出一个TimeoutCancelleationException,而withTimeoutOrNull则返回null