Kotlin高级:协程

引言
Coroutine,一个线程框架,是建立在线程之上的API,用于让代码在各个线程之间反复横跳。 你只需要理解三个概念,就能理解协程:
- 调度器
- 挂起
- 并发
创建协程
协程创建格式
|
|
- CoroutineScope协程作用域
- LaunchWay运行方式
- DisaptcherName调度器名
首先,我们来创建一个协程
|
|
注意,
|
|
叫做协程,前面的CoroutineScope.lanuch(Dispatchers.Default)对协程而言,都只的参数。
这段代码做了什么呢?接下来这段话,请慢慢细读。
CorutineScope.launch是在CurrentThread里的执行的,它会让CurrentThread中开出一个新的协程
|
|
然后让调度器Dispatchers把这个协程调度到协程上下文Main所指代的线程中运行(Main指代主线程)。
这就提到我们第一个关键——调度器。
调度器
原理

协程调度器名有:
Dispatchers.DefaultDispatchers.MainDispatchers.IODispactchers.Unconfined
分别对应协程调度器:
DefaultDisaptcherMainDispatcherIODispatcherUnconfinedDispatcher
分别调度到线程:
BackgroundThread适合CPU密集型。MainThread适合更新UI。IOThread适合I/O与网络请求。CurrentThread诶?你既然在当前线程了,直接写到当前线程下,不就好了吗?调度不是多此一举吗?注意,通过UnconfinedDispatcher调度过来的协程任务与普通的任务都区别在于,调度过来的任务会立即执行,且阻塞当前线程,只有调度过来的协程代码运行完,当前线程才会继续执行后续代码。
如图所示,我们在当前进程CurrentThread开出了多个协程Corotine,那么我们可以通过指定各个调度器名来指定使用对应的调度器,不同的调度器把协程调度到不同的线程上执行。
- 协程
Coroutine1指定调度器名Dispatchers.Default,从而使用调度器DefaultDisaptcher将协程调度到BackgroundThread。 - 协程
Coroutine2指定调度器名Dispatchers.Main,从而使用MainDispatcher将协程调度到MainThread。 - 协程
Coroutine3指定调度器名Dispatchers.IO,从而使用IODispatcher将协程调度到IOThread。 - 协程
Coroutine4指定调度器名Dispactchers.Unconfined,从而指定使用调度器UnconfinedDispatcher将协程调度到CurrentThread。
写成代码就是这样:
|
|
默认调度器
如果我们不写调度器,
|
|
就会默认使用父协程的调度器,第一处会被调度到当前线程,第二处会使用继承第一处,也被调度到当前线程。
协程与线程
协程的运行方式分为两种:
- 非阻塞式——使用
launch,不会卡住协程运行的线程。 - 阻塞式——使用
runBlocking,会阻塞协程运行的线程。
非阻塞式
在理解了调度器之后,我们来对比两段代码 不使用协程:
|
|
使用协程:
|
|

后者的代码执行过程如图所示,通过CoroutineScope.launch(Disaptcher.Default)将协程
|
|
切换到BackgroundThread执行,当前线程该干嘛就干嘛,继续执行other(),与协程内容互不相关。
划重点:在协程被调度器调度到其他线程之后,协程就完全脱离当前线程,两者互不相关。
阻塞式
阻塞式使用runBlocking来运行协程代码
|
|
他是用来霸占线程的,当我们运行上述代码,协程被运送到BackgroundThread,这里的runBlocking没有指定调度器,因此继承了父协程的上下文,也被调度到了BackgroundThread(联系上文默认调度器),那么runBlocking马上就像强盗一样霸占了BackgroundThread直到自己的协程
|
|
完成,BackgroundThread才能执行其他代码。
分发到不同线程的执行顺序
刚才的代码,三个task都在同一个线程中运行,如果我想把他们放到不同的线程中运行呢?例如:
task1()交给DefaultDisaptcher分发到BackgroundThread去执行。task2()交给IODisaptcher分发到IOThread去执行。task3()交给MainDisaptcher分发到MainThread去执行。
第一种写法
你可能首先想到这样写
|
|
它的执行顺序是这样的

第二种写法
你可能会想到这样写
|
|
它的执行顺序是这样的

第三种写法
你也可能想到这样写
|
|
它的执行顺序是这样的

第四种写法(推荐)
使用withContext,顾名思义,根据当前的协程上下文来运行。例如:
|
|
上述代码的协程上下文为CoroutineScope,则其中的withContext相当于CoroutineScope.launch。
那是不是最外层也使用?像下述代码这样?
|
|
答:无法使用。 我们刚才说了,根据当前协程上下文来运行,写在最外层,没有协程上下文,因此无法使用。
结论:
withContext只能在协程中使用。
挂起
|
|
首先,你想象自己拿着四封任务函,当前线程就是你的Boss。Boss只知道,首先去Disaptcher.Default窗口找BackgroundThread。于是指示你说:“去Disaptcher.Default窗口找BackgroundThread办理吧”。然后Boss就去处理other()任务,你则去处理这四封任务函。
我们到了Disaptcher.Default窗口排队,BackgroundThread看了看task1(),确认是自己的业务,就办完了并给task1()盖上终章。
但是task2()标明了suspend,表明要换一个窗口,但你并不知道要换哪一个,然后BackgroundThread拆开任务函,看到写着Disaptcher.IO,告诉你到Disaptcher.IO找IOThread办理,临走时他千叮咛万嘱咐,办理完了记得一定要回来找他,每张任务单都最后都需要第一个经手的业务人员盖章。你连连点头。
于是你暂时离开了BackgroundThread,BackgroundThread继续处理排在你后面的人的业务。而你又来到Disaptcher.IO窗口排队,IOThread看到task2()确实是自己的业务,就完成了,你赶紧回到了Disaptcher.Default找BackgroundThread,不过要重新排队。
终于轮到你了,BackgroundThread给task2()盖上终章,然后看了看你下一张任务单task3()标记着suspend,没错,又需要换个窗口,他又拆开任务函,看到写着Disaptcher.Main。他又指示你去Disaptcher.Main窗口找MainThread。临走前,他又嘱咐你办完后一定要回来。于是你又到Disaptcher.Main窗口找MainThread,MainThread完成了task3()。你赶紧回到了Disaptcher.Default窗口找BackgroundThread,不过又要重新排队。
终于轮到你了,BackgroundThread给task3()盖上终章,最后一个任务task4()没有suspend,BackgroundThread完成了task4(),并给task4()盖上了终章。
你的所有任务终于完成了!
这个过程如下
你每次离开BakcgroundThread都过程就叫挂起Suspend,也就是暂时离开,BakcgroundThread在你离开的期间会处理其他任务,而你办完事儿后要回到BakcgroundThread那里,这个回来的过程叫做恢复Resume。
相信学完挂起,你已经发现了协程存在的意义,他让本应通过各种“异步+回调”完成的代码用同步的方式写出来。
例如
|
|
这种回调地狱在协程中用同步的方式写出来,每一行可以看作是上一行的回调:
|
|
而可以这么爽的原因,就是因为挂起和恢复。
并发
async
再仔细看看上面那一段代码,有同学可能要问了,这个requestName()和requestImage()如果没有依赖关系,也可以并发进行呀!没错,协程也为各项任务提供了并发机制。
我们可以使用async来让协程并发工作。
|
|
通过上述修改,requestName()和requestImage()就能并发工作了,两个并发任务启动(注意,不是执行完毕,是启动),立刻会执行setInfo()。
await
有同学又要问了,万一你的setInfo()依赖requestName()和requestImage()的结果,那不会有问题了吗?
没错,所以这就需要我们的await()登场了。
|
|
我们为任务的执行结果添加上await(),协程就会等待并发任务执行完毕,再继续运行后续代码。
惰性并发
有时候我们可能并不希望我们的并发任务立刻执行,那么我们就需要运用惰性并发。
|
|
我们通过async (start = CoroutineStart.LAZY)将任务变为惰性并发任务,惰性并发任务不会立刻运行,而是在两种情况下运行:
result被await(),即其他任务需要他的结果时。result被start(),即我们手动启动它时。