协程整理的相关教程:
协程基本概念
协程就像非常轻量级的线程;线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程是基于于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的。
协程是一种非抢占式的任务调度模式,程序可以主动挂起和恢复。
协程的上下文切换是由用户控制。
而Kotlin的协程的本质上是一套对线程线程进行封装的API,这套API 隐藏了异步实现细节,让我们可以用同步的方法来写异步操作。
协程的学习
如何构建一个协程
协程的官方提供的构建器以及功能
名称 | 结果 | 描述 |
---|---|---|
runBlocking | T | 当协程运行时阻塞线程 |
launch | Job | 发起没有任何结果的协程 |
async | Deferred | 返回带有未来结果的单个值 |
produce | ReceiveChannel | 产生元素流 |
主要看前面三种:
//使用runBlocking顶层函数
runBlocking {
//TODO 这个协程的范围就是,这个代码块。
}
//使用GlobalScope单例对象调用Launch方法开启协程
GlobalScope.launch {
//TODO 这个协程的范围就是,这个代码块。
}
//使用GlobalScope单例对象调用async方法开启协程,具有并发的功能
GlobalScope.async {
//TODO 这个协程的范围就是,这个代码块。
}
runBlocking
通常是适用于单元测试的场景,在的业务开发中不会使用,因为它是线程阻塞的。
GlobalScope.launch
和使用runBlocking
的区别在于不会线程阻塞。但是在Android开发中同样不举荐这中方法,因为它的生命周期和app的生命周期不一致。
async
创建一个协程并作为Deferred的实现返回其将来的结果。使用async创建的协程具有并发的性质。
launch函数
GlobalScope.launch {
}
launch 函数,它具体的含义是:创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓协程是你传给 launch 的那些代码,这一段连续代码叫做一个协程。
launch的定义:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
launch函数是CoroutineScope的扩展方法,它是在不阻塞当前线程的情况下,启动新的协程并返回一个Job。
launch函数的三个参数:
- context参数,协程的CoroutineScope.coroutineContext上下文的附加功能。协程上下文是从CoroutineScope继承的。可以使用context参数指定其他上下文元素。如果上下文没有任何调度程序或任何其他ContinuationInterceptor,则使用Dispatchers.Default。
- start参数,启动协程启动选项。默认值为CoroutineStart.DEFAULT,默认情况下,协程将立即安排执行
启动模式
// 默认,创建后立即开始调度,调度前被取消,直接进入取消响应状态。 DEFAULT, // 懒加载,不会立即开始调度,需要手动调用start、join或await才会 // 开始调度,如果调度前就被取消,协程将直接进入异常结束状态。 LAZY, // 和Default类似,立即开始调度,在执行到一个挂起函数前不响应取消。 // 涉及到cancle才有意义 @ExperimentalCoroutinesApi ATOMIC, // 直接在当前线程执行协程体,直到遇到第一个挂起函数,才会调度到 // 指定调度器所在的线程上执行 @ExperimentalCoroutinesApi UNDISPATCHED;
- block参数,在提供的范围的上下文中将调用的协程代码。
在需要去切换线程或指定线程执行任务的时候,我们如何去切换线程的呢?
协程构建器 launch 和 async接收一个可选的CoroutineContext参数,就是上面所述的第一个参数。这个参数的类型是CoroutineContext协程的上下文,它包含了一个协程调度器 CoroutineDispatcher,协程调度器确定了协程在哪个线程或者哪些线程上执行,协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
Kotlin预置四种调度器:
- Dispatchers.IO:IO调度器,适合执行IO相关操作,IO密集型任务调度器,针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求。
- Dispatchers.Main:UI调度器,它仅限于与UI对象一起操作的Main线程,如Android的主线程
- Dispatchers.Unconfined:不指定线程,如果子协程切换线程,接下来的代码也在该线程继续执行。
- Dispatchers.Default:默认调度器,由JVM上的共享线程池支持,适合处理后台计算,CPU密集型任务调度器。适合 CPU密集型的任务。
接下来看下面的一段代码:
GlobalScope.launch(Dispatchers.Main) {
launch(Dispatchers.Main) {
Log.e(TAG, "Dispatchers.Main:${Thread.currentThread().name}" )
}
launch(Dispatchers.IO) {
Log.e(TAG, "Dispatchers.IO:${Thread.currentThread().name}" )
}
launch(Dispatchers.Default) {
Log.e(TAG, "Dispatchers.Default:${Thread.currentThread().name}" )
}
launch(Dispatchers.Unconfined) {
Log.e(TAG, "Dispatchers.Unconfined:${Thread.currentThread().name}" )
}
launch {
Log.e(TAG, "不传入参数:${Thread.currentThread().name}" )
}
}
执行的结果:
E/MainActivity@: Dispatchers.IO:DefaultDispatcher-worker-1
E/MainActivity@: Dispatchers.Default:DefaultDispatcher-worker-3
E/MainActivity@: Dispatchers.Unconfined:main
E/MainActivity@: Dispatchers.Main:main
E/MainActivity@: 不传入参数:main
上面案例中,有些指定了协程调度器,Dispatchers.IO,Mian和Default都很容易理解。如果调用 launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下文(以及调度器)。在上面的例子当中就是,从 GlobalScope.launch(Dispatchers.Main)的协程承袭了它的上下文,因此同样是主线线程。
Dispatchers.Unconfined 是一个特殊的调度器且似乎也运行在 main 线程中,但实际上, 它是一种不同的机制。Dispatchers.Unconfined 协程调度器在调用它的线程启动了一个协程,但它仅仅只是运行到第一个挂起点。挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。非受限的调度器非常适用于执行不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。(非受限的调度器是一种高级机制,可以在某些极端情况下提供帮助而不需要调度协程以便稍后执行或产生不希望的副作用, 因为某些操作必须立即在协程中执行。 非受限调度器不应该在通常的代码中使用。)
这一段话说了很多东西,但是有点理解,通过下面的案例进行理解。
GlobalScope.launch(Dispatchers.Main) {
launch (Dispatchers.Unconfined){
Log.e(TAG, "还没执行其他协程前处于的线程:${Thread.currentThread().name}" )
delay(1000)
Log.e(TAG, "执行其他协程后处于的线程: ${Thread.currentThread().name}")
}
}
执行的结果:
E/MainActivity@: 还没执行其他协程前处于的线程:main
E/MainActivity@: 执行其他协程后处于的线程: kotlinx.coroutines.DefaultExecutor
从上面的案例中在还没执行delay()前,处于的线程还是继承外部协程的上下文,但是当执行delay()之后,处于的线程不在是主线程,而是与delay()挂起函数具有相同的调度器。这里提到了挂起函数,那什么是挂起函数呢?
suspend关键字
suspend是Kotlin的关键字,用于定义一个挂起函数。这里挂起的对象是协程,当创建的协程执行到某个挂起函数的时候,这个协程就会被从当前线程挂起,换句来说就是这个协程从正在执行它的线程上脱离。脱离之后的协程并不是暂定什么都不干了,而是去执行挂起函数的代码。这里可能会有三个疑问?第一个当前线程接下来会发生什么事情?第二个就是协程从当前线程脱离了,那么协程执行挂起函数的代码的是在什么线程上执行?第三个挂起函数后面剩下的代码怎么办?。
Q:当前线程接下来会发生什么事情?
A:如果该线程是一个后台线程,那么它要么被回收,要么被重新利用继续执行后台任务。如果该线程是一个Android主线程,将继续进行刷新界面的工作。
Q:协程从当前线程脱离了,那么协程执行挂起函数的代码的是在什么线程上执行?
A:这个被脱离的协程执行挂起函数所在的线程,取决于挂起函数。注意这个并不是说取决于suspend这个关键字,而是取决于这个函数的逻辑代码。suspend关键字只是起到提醒的作用。如果想要suspend定义的函数有切换线程的效果还需要依赖于withContext这个函数进行辅助。withContext()它在挂起函数起到了自动把线程切走和切回。
Q:挂起函数后面剩下的代码怎么办?
A:挂起函数执行完之后,协程为我们做的最爽的事情就是:自动把线程切换回来,协程会帮我post一个Runnable,让剩余的代码继续回到原来的线程进行工作。
下面通过一个例子进行说明:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch(Dispatchers.Main) {
Log.e(TAG, "onCreate:执行挂起函数之前协程所处的线程:${Thread.currentThread().name}" )
Log.e(TAG, "onCreate: 开始执行挂起函数" )
suspendFunction()
Log.e(TAG, "onCreate: 执行完挂起函数协程所处的线程:${Thread.currentThread().name}" )
}
}
private suspend fun suspendFunction(){
withContext(Dispatchers.IO){
Log.e(TAG, "suspendFunction: ${Thread.currentThread().name}" )
Log.e(TAG, "suspendFunction: 挂起函数执行结束" )
}
}
执行的结果:
E/MainActivity@: onCreate:执行挂起函数之前协程所处的线程:main
E/MainActivity@: onCreate: 开始执行挂起函数
E/MainActivity@: suspendFunction: DefaultDispatcher-worker-1
E/MainActivity@: suspendFunction: 挂起函数执行结束
E/MainActivity@: onCreate: 执行完挂起函数协程所处的线程:main
在上面的例子中,我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;当这个函数执行完毕后,线程又切了回来,切回来也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。
总的来说就是协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;
不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程。
需要注意的是,suspend 关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。创建一个 suspend 函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数。不然只是suspend在函数在加上一个关键字suspend是没有意义的。另外suspend函数要么在协程中调用,要么在另一个挂起函数中调用,不然在非协程和非挂起函数中调用,否则就会编译不通过。
async函数
async 类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred。可以使用 .await() 在一个延期的值上得到它的最终结果。Deferred是Job的子类,同样具有Job的部分功能。
使用async并发
当有两个挂起函数需要进行请求远程服务时或者需要进行大量的计算时,这个时候如果我们按如下方式调用它们:
fun main(){
runBlocking {
val time = measureTimeMillis {
val one= doSomethingOne()
val two= doSomethingTwo()
println("结果为:${one+two}")
}
println("总共所要的时间:$time")
}
}
suspend fun doSomethingOne():Int{
//进行耗时的操作或计算
delay(1000)
return 1
}
suspend fun doSomethingTwo():Int{
//进行耗时的操作或计算
delay(1000)
return 2
}
执行的结果:
结果为:3
总共所要的时间:2031
在上面的代码中我们顺序调用doSomethingOne和doSomethingTwo的两个挂起函数,他们在执行的时候时按照默认的顺序进行执行,也就时说先执行完doSomethingOne之后,在开始执行doSomethingTwo。实际上,我们没必要这样做,如果doSomethingTwo需要依赖doSomethingOne的结果的时候我们才需要这么做。如果两个并不依赖,这样做就会花费更多的时间。这时候我们会想到并发,协程的提供了async函数,可以实现并发执行。将上面的代码进行修改:
fun main(){
runBlocking {
val time = measureTimeMillis {
val one= async { doSomethingOne() }
val two= async { doSomethingTwo() }
println("结果为:${one.await()+two.await()}")
}
println("总共所要的时间:$time")
}
}
suspend fun doSomethingOne():Int{
//进行耗时的操作或计算
delay(1000)
return 1
}
suspend fun doSomethingTwo():Int{
//进行耗时的操作或计算
delay(1000)
return 2
}
执行的结果
结果为:3
总共所要的时间:1013
通过上面的两个时间相比,可以明确知道使用async并发时间快了很多。
惰性启动的async
a sync 可以通过将 start 参数设置为 CoroutineStart.LAZY 而变为惰性的。 在这个模式下,只有结果通过 await 获取的时候协程才会启动,或者在 Job 的 start 函数调用的时候。
fun main(){
runBlocking {
val time = measureTimeMillis {
val one= async(start = CoroutineStart.LAZY) { doSomethingOne() }
val two= async(start = CoroutineStart.LAZY) { doSomethingTwo() }
println("结果为:${one.await()+two.await()}")
}
println("总共所要的时间:$time")
}
}
suspend fun doSomethingOne():Int{
//进行耗时的操作或计算
delay(1000)
return 1
}
suspend fun doSomethingTwo():Int{
//进行耗时的操作或计算
delay(1000)
return 2
}
执行的结果:
结果为:3
总共所要的时间:2036
惰性的启动需要显示调用.await或者Job的start方法才会开启,这样失去了并发的效果。
需要注意的是,如果在上面的代码中,如何有one和two其中出现了异常,那么会发生什么事情呢?如果其中一个发生异常,那么在同一作用法域的协程都会取消,其父协程也会取消。例如,如果two发生异常,one还没执行完,one会被取消,而等待中父协程也会被取消。
Job
利用launch函数创建一个协程的时候,返回一个Job对象,代表一个协程的工作任务。有下面常用的API
/**
* 协程状态
*/
isActive: Boolean //是否存活
isCancelled: Boolean //是否取消
isCompleted: Boolean //是否完成
children: Sequence<Job> // 所有子作业
/**
* 协程控制
*/
cancel() // 取消协程
join() // 堵塞当前线程直到协程执行完毕
cancelAndJoin() // 两者结合,取消并等待协程完成
cancelChildren() // 取消所有子协程,可传入CancellationException作为取消原因
attachChild(child: ChildJob) // 附加一个子协程到当前协程上
cancel 取消协程
可以通过协程的返回对象Job来取消协程:
fun main(){
runBlocking {
val job =launch {
repeat(1000){
println("Job:执行了 $it")
delay(1000)
}
}
delay(3000)
println("准备取消Job")
job.cancel()//取消作业
job.join()//等待Job的执行结束
println("Job,已被取消")
}
}
执行的结果:
Job:执行了 0
Job:执行了 1
Job:执行了 2
准备取消Job
Job,已被取消
上面的结果中,执行了三次就停止,如果我们没有调用cancel()取消,那么协程会一直运行下去。
接下来,看一下取消协程需要注意的地方:
取消作用域会取消它的所有子协程,也就是说父协程被取消了,它的子协程都会被取消。
通过下面的代码验证这个一点。
runBlocking {
val parentJob= launch {
val child1=launch {
repeat(1000){
println("child1:执行了 $it")
delay(1000)
}
}
val child2=launch {
repeat(1000){
println("child2:执行了 $it")
delay(1000)
}
}
}
delay(3000)
println("准备取消父协程")
parentJob.cancel()//取消父协程
//查看子协程的状态
parentJob.children.iterator().forEach {
if (it.isCancelled){
println("子协程被取消")
}
}
parentJob.join()//等待Job的执行结束
println("父协程,已被取消")
}
执行的结果:
child1:执行了 0
child2:执行了 0
child1:执行了 1
child2:执行了 1
child1:执行了 2
child2:执行了 2
准备取消父协程
子协程被取消
子协程被取消
父协程,已被取消
从上面的结果可以知道,当父协程被取消的时,它的作用域中的两个协程不再执行,也是被取消。
同一作用域中,被取消的子协程不会影响其余兄弟协程;
在同一作用域中,平级的协程被取消不会影响到其他的同级的协程。
fun main(){
runBlocking {
val parentJob= launch {
val child1=launch {
repeat(1000){
println("child1:执行了 $it")
delay(1000)
}
}
val child2=launch {
repeat(1000){
println("child2:执行了 $it")
delay(1000)
}
}
delay(2000)
//取消第一个子协程
child1.cancel()
child1.join()
println("取消第一个子协程")
}
}
}
执行的结果:
child2:执行了 0
child1:执行了 1
child2:执行了 1
取消第一个子协程
child2:执行了 2
child2:执行了 3
child2:执行了 4
child2:执行了 5
.
.
.
.
从上面的结果可以看到,当第一个子协程被取消的时候,其他的兄弟协程并没有被取消,而且继续进行自己的工作。
取消是协作的
协程的取消是协作式的,协程不会在调用cancel()时立即停止,调用后只是进入取消中状态,只有工作完成后才会变成已取消状态,所以需要我们在代码中定期检查协程是否处于活动状态。比如下述例子:
runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: 执行了${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("准备取消协程")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("协程取消完成")
}
执行的结果:
job: 执行了0 ...
job: 执行了1 ...
job: 执行了2 ...
准备取消协程
job: 执行了3 ...
job: 执行了4 ...
协程取消完成
从上面的结果可以看到,当调用了cancel()是协程并没有立即停止工作,而是还执行了两次。为了可以让协程在取消的时候停止,工作需要我们在代码中定期检查协程是否处于活动状态。可以使用isActive来检查状态:
fun main(){
runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) {
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: 执行了${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("准备取消协程")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("协程取消完成")
}
}
执行结果:
job: 执行了0 ...
job: 执行了1 ...
job: 执行了2 ...
准备取消协程
协程取消完成
另外也可以通过yeid()来实现效果,yeid()可以简单理解为起当前任务(注意是任务),释放此线程的monitor让其他正在等待的任务公平的竞争,去获得执行权。
处理协程取消的副作用
当我们要在协程取消后执行某个特定的操作,比如关闭可能正在使用的资源,或者是针对取消需要进行日志打印,又或者是执行其余的一些清理代码。我们可以通过下面的方式做到这一点:
- 检查 !isActive
可以定期地进行 isActive 的检查,那么一旦您跳出 while 循环,就可以进行资源的清理。
- Try catch finally
因为当协程被取消后会抛出 CancellationException 异常,我们可以将挂起的任务放置于 try/catch 代码块中,然后在 finally 代码块中执行需要做的清理任务。
处于取消中状态的协程不能够挂起
那么如果当协程被取消后需要调用挂起函数,我们需要将清理任务的代码放置于 NonCancellable CoroutineContext 中。这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。
fun main(){
runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
try {
work(startTime)
}catch (e:CancellationException){
println("work 已取消")
}finally {
withContext(NonCancellable){
delay(1000)
println("清理工作。。")
}
}
}
delay(1300L) // 等待一段时间
println("准备取消协程")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("协程取消完成")
}
}
private suspend fun work(startTime: Long) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
yield()
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: 执行了${i++} ...")
nextPrintTime += 500L
}
}
}
执行结果:
job: 执行了0 ...
job: 执行了1 ...
job: 执行了2 ...
准备取消协程
work 已取消
清理工作。。
协程取消完成
withContext(NonCancellable)的NonCancellable它设计用于withContext函数防止取消需要执行而无需取消的代码块。
协程的取消和异常可以参考以下两篇文章:
借助 scope 来取消任务
在 Kotlin 中,定义协程必须指定其 CoroutineScope 。CoroutineScope 可以对协程进行追踪,即使协程被挂起也是如此。与的调度程序 (Dispatcher) 不同,CoroutineScope 并不运行协程,它只是确保您不会失去对协程的追踪。
为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用 CoroutineScope 的情况下启动新的协程。CoroutineScope 可被看作是一个具有超能力的 ExecutorService 的轻量级版本。
CoroutineScope 会跟踪所有协程,同样它还可以取消由它所启动的所有协程。这在 Android 开发中非常有用,比如它能够在用户离开界面时停止执行协程。
在Android上使用协程
在 Android 平台上,可以将 CoroutineScope实现与用户界面相关联。这样可以避免泄漏内存或者对不再与用户相关的 Activities 或 Fragments 执行额外的工作。当用户通过导航离开某界面时,与该界面相关的 CoroutineScope 可以取消掉所有不需要的任务。而GlobalScope并不能满足这一点,如果在Activity或者Fragment中使用GrobalScope并且没有主动去取消的时候,即使Activity或Fragment已经被销毁,协程仍然在执行。这个时候我们可以使用生命周期感知的协程范围。
生命周期感知的协程范围
- viewModelScope
在ViewModel中使用viewModelScope来创建一个协程,如果ViewModel 清除,则在此范围内启动的所有协程都会自动取消,无需手动去取消这个协程,通过使用 viewModelScope,可以确保所有的任务,包含死循环在内,都可以在不需要的时候被取消掉。
- lifecycleScope
在Actvity或Fragment中应该使用lifecycleScope,不建议使用GlobalScope,当 LifeCycle 回调 onDestroy() 时,协程作用域 lifecycleScope 会自动取消,协程作用域 lifecycleScope 会自动取消。而且lifecycleScope还提供了一些可以指定至少在特定的生命周期之后再执行挂起函数,可以进一步减轻 View 层的负担。
//当控制此LifecycleCoroutineScope的Lifecycle至少处于[Lifecycle.State.CREATED]状态时,
// 才启动去执行协程。
fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenCreated(block)
}
//当控制此* LifecycleCoroutineScope的Lifecycle至少处于[Lifecycle.State.STARTED]状态时,
// 才启动去执行协程。
fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenStarted(block)
}
// 当控制此LifecycleCoroutineScope的Lifecycle至少处于[Lifecycle.State.RESUMED]状态时,
// 才启动去执行协程。
fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenResumed(block)
}
网友评论