协程
概念
-
协程本质上可以认为是运行在线程上的代码块,是一套有kotlin官方提供的线程API(类似Java的Executor) 不用过多关注线程也能写出并发操作。(线程框架)
-
import kotlinx.coroutines.* fun main() = runBlocking { repeat(100_000) { // launch a lot of coroutines launch { delay(5000L) print(".") } }import kotlin.concurrent.thread fun main() = runBlocking { repeat(100_000) { // launch a lot of coroutines thread { Thread.sleep(5000L) print(".") } } }It launches 100K coroutines and, after 5 seconds, each coroutine prints a dot. Now, try that with threads. What would happen? (Most likely your code will produce some sort of out-of-memory error)
但是实际上,上面的thread是把所有内容放到了一个线程池,用协程和100000个threads进行比较性能,所以才会用thread才会导致内存不足,因为开启一个线程非常的消耗资源。同时,sleep() 和 delay()本身也是不一样的,sleep()是占用线程,delay()不会占用线程
可用coroutine和executor进行比较,两个性能都差不多。两个都是基于线程造出来的工具包。
- 总结:
- 协程:切线程
- 挂起:可自动切回来的的切线程
- 非阻塞式挂起:用看起来阻塞的代码写非阻塞。
好处
- 方便(基于kolin的语言优势,所有用起来会比基于Java的方案会更方便一些)
- 可以用看起来同步方式写出异步代码,不需要回调
//回调方式
api.getCard(canId)
.enqueue(obj : Callback<Bitmap> {
...
override fun onResponse(call:Call<Bitmap>,
response: Resoinse<Bitmap>) {
val card = resonse.body()
api.getSofList(canId)
.enqueue(obj : Callback<Bitmap> {
...
override fun onResponse(call:Call<Bitmap>,
response: Resoinse<Bitmap>) {
val sofList = response.body()
show(suspendMerge(cardDetail, sofList))
})
}
})
api.getUser(new Callback<User>() {
@Override
public void success(User user) {
runOnUiThread(new Runnable() {
@Override
public void run() {
nameTv.setText(user.name);
}
})
}
@Override
public void failure(Exception e) {
...
}
});
//协程
launch(Dispatchers.Main) {
val cardDetail = async {api.getCard(canId)} // 获取卡详情
val sofList = async {api.getSofList(canId)} // 获取银行卡信息
val cardInfo = suspendMerge(cardDetail.await, sofList.await) // 合并两个信息
show(cardInfo) // UI显示
}
👆的两个网络请求是可以并行的网络请求,只是他们需要一起展示出来。如果用回调的方法,就会被做成一个串行的,然后网络等待多了一倍。而kotlin的协程,只用写为上下两行代码结构,然后在第三行合并。
如果网络请求更复杂,协程依然可以很清晰的写出来。 消除了并发任务间的协作难度,能很简单的实现并行
如何创建
launch async
runBlocking : 线程阻塞式的,试用与单元测试场景
| 标题 | async | launch |
|---|---|---|
| 相同 | 启动协程,返回Coroutine | 启动协程,返回Coroutine |
| 不相同 | 返回的Coroutine多实现了Deferred接口 |
- launch
launch(Dispatchers.IO){} 创建一个新的协程,并在IO线程上执行它,
而协程就是 {} 里面的代码
launch(Dispatchers.Main){
withContext(Dispatchers.IO) { ... }
withContext(Dispatchers.IO) { ... }
}
//切换线程也可以通过上下两行代码结构实现,不用嵌套
//还可以用suspend函数将withContext(Dispatchers.IO)提出到另外一个方法
suspend fun getCard() = withContext(Dispatchers.IO) {
api.getCard()
}
launch(Dispatchers.Main){
withContext(Dispatchers.IO) { ... }
getCard()
}
withContext 这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行.
// 用 withContext 和 不用的区别
coroutineScope.launch(Dispatchers.IO) {
...
launch(Dispatchers.Main){
...
launch(Dispatchers.IO) {
...
launch(Dispatchers.Main) {
...
}
}
}
}
// 通过第二种写法来实现相同的逻辑
coroutineScope.launch(Dispatchers.Main) {
...
withContext(Dispatchers.IO) {
...
}
...
withContext(Dispatchers.IO) {
...
}
...
}
- Async
coroutineScope.launch(Dispatchers.Main) {
val card: Deferred = async { api.getCard(canId) }
val sofList: Deferred = async { api.getSofList(canId)}
// update UI
show(sofList.await(), card.await())
}
//await的签名
public suspend fun await(): T
挂起
-
挂起的对象是协程
-
launch(){} 里面的代码就是协程,当执行到某个suspends函数/挂起函数时, 这个协程就被挂起了 → 从正在执行他的线程上脱离出来。→ 从这一行代码开始,这个协程所在的线程不再运行这个协程了。
-
然后该线程
启动一个在主线程的协程,实际上就是往主线程post了一个任务
Runnable(即协程代码),那当协程挂起以后,实际上就是post()这个任务提前结束了。这个时候主线程就该做什么做什么,例如继续刷新界面(安卓的主线程),或者就被回收/再利用(后台任务)launch(Dispatchers.Main) { val user = suspendGetCard(canId) status = card.status } //相当于 handler.post{ val card = api.getCard(canId) status = card.status } -
该协程
函数代码在运行到挂起函数/suspend函数的时候被掐断了,所有接下来会从挂起函数开始继续向下在指定的线程里执行,
launch(Dispatchers.Main) { val card = suspendGetCard(canId) status = card.status } //指定到IO线程, suspend fun suspendGetUser(canId: Int) { withContext(Dispatchers.IO) { api.getCard(canId) } } -
然后在所有任务都完成以后,协程将自动切回主线程,让剩下的代码回到主线程去执行。这也是为什么用Dispatcher调度器来指定线程,它不仅需要切到指定线程执行任务,还要再切回来。
so: 挂起就是 暂时切走,一会儿再切(resume)回来。(稍后会被自动切resume回来的线程切换)
Dispatcher调度
Dispatcher.Main: 安卓的主线程, 通常UI渲染在这里[Dispatcher.IO](http://dispatcher.IO):IO工作, 读写文件,网络请求,数据库操作Dispatcher.Default:cpu工作: 计算
那切回来的意思就是协程会再post一个任务
Runnable,让主线程继续执行剩余的代码 -
-
suspend函数只能在协程里或者另一个挂起函数里启动
- 首先,挂起函数一会儿是需要切回来的,而恢复/切回来/resume这个功能是协程的,所以如果不在协程中启动,就恢复不了。
- 然后一个挂起函数如果在一个协程/挂起函数中启动,它终归是在一个协程里面被调用的,为的就是让协程可以在挂起函数切换线程后再切回来。
- 如何挂起?
如果是👆这样的, 最终代码还是运行在Main线程里的,因为它不知道往哪儿切。(且android studio会告诉你suspend是多余的)
fun getCard(canId : String, dataSource: DataSource) {
CoroutineScope(Dispatchers.Main).launch{
suspendGetCard(canId,dataSource)
}
}
private suspend fun suspendGetCard(canId : String, dataSource: DataSource) {
withContext(Dispatchers.IO) {
dataSource.getCard(canId)
}
}
withContext 就是一个挂起函数,然后Dispatcher参数让withContext知道往哪儿切,然后协程才被挂起了,所以实际协程是在withContext才被挂起的,函数前面的 suspend 并没有让协程挂起或者切线程。是suspend函数执行到内部自带的挂起函数的时候挂起的。
- suspend关键字作用:提醒
- 函数的创建者对函数调用者的提醒: 我是一个耗时的任务,因此我被我的创建者用挂起的方式放在了后台运行, 所以请在协程里调用我。
- 这个提醒就是为了让主线程不卡 ,调用者不用去做这个耗时的切线程的工作,而是创建者去做,调用者只需要接受一个提醒。
- 这种提醒就形成了一种机制,即所有耗时任务都自动放到后台执行的机制,所有主线程就不卡了。
总结: suspend关键字只是一个提醒,实际进行挂起操作的是挂起函数里面的实际代码。
- 怎么自定义suspend函数
- 什么时候自定义?→耗时的
-
I/O操作 或者 计算工作:
文件读写,网络交互,图片的模糊,美化处理
-
本身并不慢,但是比如需要先等待几秒再执行
-
- 怎么写?withContext 最常用
suspend fun a(){
withContext(){}
}
// delay()
非阻塞式挂起
非阻塞是:不卡线程
线程阻塞:
- 前面有障碍,过不去,线程卡了
- 等待清除障碍,耗时任务结束
- 或者绕道,切换线程
协程中的挂起就是指的非阻塞式的挂起,非阻塞就是挂起的一个特点。且阻塞非阻塞都是基于单线程而言的,多线程就不存在阻塞问题了。因此,非阻塞式挂起就是说挂起的同时切换线程。