0%

Kotlin之协程-扔物线笔记

通过扔物线对于Kotlin协程的讲解介绍,对协程有了更加清晰的认识

简单定义

一个封装了线程的上层线程API,用于线程切换,后台执行耗时任务

简单使用

协程Scope.launch {}

1
2
3
4
5
6
launch {
val imgae = getImageFromIO(imageId)
imageView.setImageBitmap(image)
}
// launch方法需要使用某个Scope(作用域)调用,或者在runBloking {}中调用
// launch方法可以携带,指定目标线程;不携带参数时默认在本线程执行

概念及优势

一个类似Java的Executor,Android的AsycTask的线程框架。
区别在于,利用Kotlin的语言优势,可以更方便实现线程切换,以及写出看似阻塞(同步)的非阻塞(异步)代码,即Kotlin的非阻塞式挂起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
launch(Dispatchers.Main) {
val imgae = getImageFromIO(imageId)
imageView.setImageBitmap(image)
}
````
协程可以实现在一个代码块中编写不同线程的代码,即getImageFromIO()方法,可以切到后台进行耗时操作,然后切回UI线程进行界面操作。而如果是在Java中,这部分代码一般会使用回调实现,然后在耗时任务完成的回调中进行界面操作。相比之下,协程的代码逻辑就看似顺序,同步了起来。
> 通过消除回调,抹平了多线程开发的难度,甚至打破回调对开发能力的限制
> 如当有这样的需求:通过网络获取两个没有相关性的接口信息内容,本地进行合并后显示。
> 理论上没有相关性的接口,应该使用并行请求,然后本地作融合,然而如果是回调式实现,就会是先发第一个请求,回调中进行第二个请求,然后再在回调中合并显示,时间上看就是双倍的请求时间,即降低了一倍的性能。
> 但在kotlin可以这样实现。
``` kotlin
launch(Dispatchers.Main) {
val info1 = async { api.getInfo1() }
val info2 = async { api.getInfo2() }
val info = merge(info1.await(), info2.await())
show(info)
}

使用async也可以创建一个协程,并且与launch相同,可以返回一个Coroutine对象。区别在于,async返回的Coroutine对象还实现了Deferred接口,使得可以通过调用Deferred.await()方法可以获得async任务结束后返回的结果
因此,使用协程最重要的目的是,实现在一个代码块中进行多次的线程切换,为了改变并发任务的操作难度,降低复杂并发任务的实现难度甚至实现Java中难以实现的任务

使用方式

使用launch包含一个代码块,实现创建一个协程并且指定一个线程执行。所谓协程就是代码块内容,所以可以理解为一个任务。

withContext也可以实现线程切换执行任务,任务执行完成后切回原线程的功能。自动切回原线程的能力,大大减少了代码的嵌套,同时便于功能的封装。

withContext()作为一个suspend函数,需要在协程中使用或在另一个suspend函数中使用。给函数加上suspend修饰,函数即为suspend函数,标识该方法执行一个耗时任务,建议(要求)在协程中执行。

suspend即为暂停,挂起,即代码执行suspend会进行挂起操作,并且是非阻塞式挂起。该操作其实是进行了一个线程切换的工作,因此非阻塞式是相对于原线程来说,没有受到阻塞。

我理解的withContext和launch的区别在于,launch可以创建一个协程,并且指定线程执行;withContext方法是一个suspend方法,只能在协程中调用,用于线程的切换并且执行完毕后会切回原线程。

协程的挂起

launch创建的协程,执行到suspend方法时,会进行挂起操作。
所谓挂起,是将当前协程从线程上挂起,脱离原线程执行,即进行了线程切换操作。协程挂起后,不再在原线程执行,原线程的代码块(launch代码块)返回(return,类似提前结束)。挂起的协程,在执行的线程继续执行, 结束了suspend函数的内容以后,协程切回原线程(resume),执行剩余代码。
与handler.post机制类似,launch相当于往handler post了一个任务A执行,执行到suspend函数时,任务A提前结束返回(挂起),handler执行其他任务,suspend函数的内容在指定函数执行,工作内容结束以后,任务A剩余的工作再次被post到handler中执行(resume)。
所以所谓挂起,即可以理解为,一个暂时切走,然后会再切回来的线程切换

因此,因为协程有了resume恢复功能,会自动把挂起的线程切回来,所以suspend函数必须在协程中使用,保证协程被切走后,能被切回来。

如何挂起

定义一个suspend函数并且在其中使用withContext()函数(协程自带的,具备协程挂起功能的函数),线程切换发生在withContext中。
因此单纯的定义suspend并不能实现协程挂起,suspend只是一个函数定义者对该函数工作的标识,表示该函数是一个耗时函数,必须在协程中调用。
仅是对一个函数标识suspend却没有在其中使用挂起函数,只能实现限制该函数在协程中使用但并没有线程切换的能力。
本质上是一个要求,实际上是一个提醒,尝试实现使所有的耗时操作都在后台执行的机制,降低主线程的卡顿。
综上,当一个函数中会进行耗时或者等待操作,则需要对其标识suspend,并且在其中进行挂起操作

关于launch

launch用于创建一个协程并返回一个Job对象,Job对象即可视为一个协程。其余创建协程的方法有launch,async,runBlocking等。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Android
Log.e(tag, "0:${Thread.currentThread().name} ${Thread.currentThread().id}")
lifecycleScope.launch {
Log.e(tag, "1:${Thread.currentThread().name} ${Thread.currentThread().id}")
withContext(Dispatchers.IO) {
Log.e(tag, "2:${Thread.currentThread().name} ${Thread.currentThread().id}")
}
delay(3000)
Log.e(tag, "3:${Thread.currentThread().name} ${Thread.currentThread().id}")
}
Log.e(tag, "5:${Thread.currentThread().name} ${Thread.currentThread().id}")

// MacOS Windows
runBlocking {
println("0:${Thread.currentThread().name} ${Thread.currentThread().id}")
val job = GlobalScope.launch {
println("1:${Thread.currentThread().name} ${Thread.currentThread().id}")

withContext(Dispatchers.IO) {
println("2:${Thread.currentThread().name} ${Thread.currentThread().id}")
}
delay(3000)
println("3:${Thread.currentThread().name} ${Thread.currentThread().id}")
}
println("5:${Thread.currentThread().name} ${Thread.currentThread().id}")
job.join()
}

使用以上代码进行测试,分别基于MacOS的kotlin环境,Windows10的Kotlin环境,以及Android系统环境进行测试,得到了不同的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ---- Android ----
// 0:main 1
// 1:main 1
// 5:main 1
// 2:DefaultDispatcher-worker-1 14868
// 3:main 1

// ---- Mac0S ----
// 0:main @coroutine#1 1
// 5:main @coroutine#1 1
// 1:DefaultDispatcher-worker-1 @coroutine#2 11
// 2:DefaultDispatcher-worker-2 @coroutine#2 12
// 3:DefaultDispatcher-worker-2 @coroutine#2 12

// 0:main @coroutine#1 1
// 5:main @coroutine#1 1
// 1:DefaultDispatcher-worker-1 @coroutine#2 11
// 2:DefaultDispatcher-worker-2 @coroutine#2 12
// 3:DefaultDispatcher-worker-2 @coroutine#2 11

// ---- Windows ----
// 0:main 1
// 5:main 1
// 1:DefaultDispatcher-worker-1 @coroutine#2 14
// 2:DefaultDispatcher-worker-2 @coroutine#2 14
// 3:DefaultDispatcher-worker-2 @coroutine#3 15

可以看到,只有在Android平台下的表现才是符合上述的理论的,即launch没有进行线程切换,只有在withContext()中才进行线程切换。然而其他平台都是进入协程代码块(launch)即进入了新的线程。
起初我还以为,是因为不同操作系统的线程调度机制差异,因此协程作为线程调度的上层API也产生了差异。实际上不是这样。
网上有文章说GlobalScope启动的协程都是独立的,生命周期只受到Application影响。因此使用GlobalScope.launch创建的协程也会在一个独立的线程执行。
使用coroutineScope { }可以为其中的协程添加作用域,而该协程任务不会在后台线程中执行。


未完 待续 (╯‵□′)╯︵┻━┻