协程作为Kotlin最重要的特性之一,自然有必要深挖一番。但在动手分析前我们需要先对协程的概念有一定了解。有了设计思想的指导,我们在看具体实现的时候自有拨云见日之感了。
协程 —— 回调与IO
以一个常见的用户登录场景为例,使用线程可能是下面这样
loginBtn.setOnClickListener {
NetUtils.sendLoginRequest(object : LoginCallback {
override fun onSuccess(userInfo: UserInfo) {
runOnUiThread {
userNameTv.text = userInfo.name
}
}
override fun onError(e: Exception) {
e.printStackTrace()
}
})
}
为了免于阻塞主线程,网络请求的处理一般会放在一个新线程中,收到数据后在处理完成的回调中再回调到主线程更新界面。那么这里就出现了回调这种手段,来实现在当前需要的数据缺失的情况下,延后至条件满足再继续执行的场景。
所以关键之处就在于“等待”这一步上。等的可能是网络端口传过来的数据,可能是从本地磁盘上加载的一张数据库表,可能是极其复杂的图形渲染运算,可能是其他线程正在持有的锁,甚至是一个还未发生的UI操作。
我们看到,同样是等待,却也不尽相同。大致上可以分为两类:IO阻塞与非IO阻塞。为什么这样划分呢,因为IO阻塞有一个特点:不会占用CPU资源。我们知道,在CPU单核上跑的多线程,依赖于操作系统的调度,每个线程都参与竞争,竞争成功的线程获得时间片继续运行,之后继续竞争,以此让每个线程都有机会执行。而在调用了IO的阻塞API后,当前线程等待内核准备数据,不再往下执行,CPU也会转为运行其他线程,等到内核缓冲区准备好后,当前线程再重新参与线程竞争,往下执行。
可以看出,线程在被系统IO调用阻塞的时间段内其实并没有工作,如果能将这段时间利用起来那么意味着在IO密集的场景下对处理速度是一个巨大的提升。
IO多路复用正是相当契合的一种技术。它能够收集监听多个文件描述符上的事件,并在有事件到来时通知相应的注册者,而这种通知机制正是用的回调。这就意味着每次注册要监听的文件描述符时就要同时注册一个回调,这无疑会造成模板代码。其次如果回调中还有其他异步操作,就又会形成回调,当涉及多个模块或者服务时就有可能形成回调地狱,令人头大。
那么,有没有一种在执行条件不满足时停止,又能在需要时恢复执行,同时形式上摆脱回调地狱的方法呢?协程说:没错,正是在下。
协程的概念早已有之,它作为一个执行体,具有挂起和恢复两种操作,并且在被挂起时能保存当前执行的状态和中间结果,并在下一次恢复的时候利用这些保存的状态和数据接着往下执行。
而当它和IO多路复用结合在一起时,我们可以把IO的读写操作用一个协程包裹起来,由于IO操作不再是阻塞式的,那么只需要收集监听对应的文件描述符,并这个协程挂起,然后去执行下一个协程。当检测到该文件描述符上有目标事件时,再恢复之前挂起的协程,让代码继续往下执行。
这样似乎隐约能感受到一点协程的魅力了,但还是有些抽象,心中可能会有几个疑问:
- 协程体是什么
- 什么是挂起,什么是恢复
- 协程体是怎么调度的
- 协程体是何时挂起的
- 协程体是怎么保存中间状态的
- 恢复的时候是怎么继续执行的,怎么调度
协程的实现体现在各个语言都有所不同,我们主要来深入分析一下kotlin的协程实现,看看它和我们上面的定义有哪些区别。