前言
- 协程是轻量级的线程
- 协程又称微线程
- 协程(Coroutines),是基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做用户空间线程,具有对内核来说不可见的特性。
- 协程,又叫纤程(Fiber),或者绿色线程(GreenThread)
协程是一种协作式的代码程序,可以让开发人员自己去进行人为控制的一种框架。一种并发理念,在Kotlin开发语言的协程还是和其他平台不同的。
协程、协作
Kotlin的协程是在线程基础上封装的线程框架。
JVM当中,线程是CPU调度的基本单位,它运行在内核态,线程的执行不能靠Java开发人员的人为的干预,即便可以提高线程的优先级,也不能保证线程一定优先执行,线程是抢占式的。(线程和协程不是同级别的)
kotlin
// 定义一个线程,优先级为50,并且启动它
thread(priority = 50) {
Log.i(TAG,"start thread 1")
}
// 定义一个线程,优先级为100,并且启动它
thread(priority = 100) {
Log.i(TAG,"start thread 2")
}
经过几次运行,他们的顺序是不能保证的。所以线程是抢占式的。
kotlin
// 定义一个线程,优先级为50,并且启动它
thread(priority = 50) {
Log.i(TAG,"start thread 1")
// 定义一个线程,优先级为100,并且启动它
thread(priority = 100) {
Log.i(TAG,"start thread 2")
}
}
这样写可以保证线程的执行顺序。
kotlin
fun main()= runBlocking {
// 启动第一个协程
val job1 = launch(Dispatchers.Default) {
repeat(10){
log("job1:$it")
delay(200)
}
}
// 等待协程1完成
job1.join()
// 启动第2个协程
val job2 = launch(Dispatchers.Default) {
repeat(10){
log("job2:$it")
}
}
// 等待协程2完成
job2.join()
log("main All jobs are completed.")
}
控制协程执行前后顺序。协程本身是基于线程的,它的执行顺序也是依赖线程的,在开发层面上,协程是一个封装好的线程池框架,它提供丰富的API结合kotlin编译器语法帮助更好处理异步协助编程。之前每协程的时候,依赖线程池、Handler、AsyncTask、IntentService等等异步编程的框架来实现我们的代码,在协助方式上,协程帮我们封装了各个可能遇到的场景的线程框架。
协程保证协作的重要的两个概念,挂起和恢复。
挂起和恢复
线程挂起是指暂停当前线程的执行,将控制权交给其他线程或进程。通常发生在多线程中,当一个线程需要等待某个条件成立(例如:等待资源可用或等待其他线程完成工作)时,它可以选择挂起自己,让出CPU给到其他线程使用。
使用wait和notify/notifyAll组合线程挂起和恢复。
java
public static void test(){
// 定义一个lock变量来实现锁的竞争
final Object lock = new Object();
Thread thread1 = new Thread(() -> {
// 线程1获取锁
synchronized (lock){
try{
log("thread1 wait");
lock.wait();
log("thread2 resumed");
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
// 线程2获取锁
synchronized (lock) {
try {
log("thread2 sleep 5seconds");
Thread.sleep(5000);// 模拟耗时操作
log("thread2 notify");
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
kotlin
fun main()= runBlocking {
ThreadExample.test()
// 防止父线程过早退出
Thread.sleep(5500)
}
输出:
thread1 wait
thread2 sleep 5seconds
thread2 notify
thread2 resumed
不过,java的wait实际上是会阻塞当前线程。
kotlin
// 定义请求方法
fun requestHttp(
requestParams : Map<String,String>,
block : (RequestResult) -> Unit
) = requestHttpReal(requestParams,block) // 这里模拟真实的请求
kotlin
val requestParams = assembleRequestParams() // 这里是主线程
thread { // 这里会"挂起"当前的线程
requestHttp(requestParams){ // 这里是IO线程,进行http请求
requestResult->{
runOnUiThread{ updateUI(requestResult) } // 这里会"恢复"当前的线程,切换回主线程,进行UI刷新
}
}
}
doSomethingElse()
看似主线程被挂起,其实主线程还在执行(下面的doSomethindelse)。
协程的挂起和恢复
在需要的方法上加上suspend关键字。
kotlin
suspend fun requestHttp(
requestParams : Map<String,String>,
block : (RequestResult) -> Unit
) = requestHttpReal(requestParams,block) // 这里模拟真实的请求
val requestParams = assembleRequestParams()
GlobalScope.launch{ // 开启一个全局协程Coroutine-1(实际开发环境当中不建议使用) ,在main主线程
val requestResult = withContext(Dispatchers.IO){ // 开启一个协程Coroutine-2,并将该协程放在异步线程里面执行,并挂起Coroutine-1
requestHttp(requestParams) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒),该方法用suspend关键字修饰
}
updateUI(requestResult) // 恢复Coroutine-1的执行,进行UI刷新
}
doSomethingElse()
kotlin编译器结合协程,让我们用同步的方式写异步代码(updateUi会等待withContext里协程执行完耗时操作后才执行),避免回调地狱。
- 使用GlobalScope.launch创建协程Coroutine-1,尽管这个协程在主线程运行,但是他具备了协程的挂起和恢复的能力。
- 当执行withContext(Dispatchers.IO)时候,开启了第二个协程Couroutine-2
- 因为withContext函数的特性,它会挂起当前协程(Coroutine-1),此时会将我们的执行点进行一个上下文记录(包括协程执行的代码点,所处的线程、它的上下文数据),也就是updatUI之前,停止(协程coroutine-1)的执行,转而执行Coroutine-2里面的代码,也就是requestHttp的代码
- 由于withContext里指定的调度器是Dispatchers.IO,协程框架会自动进行线程的切换
- 等待requestHttp代码执行完毕,根据之前Coroutine-1的记录的代码点和所处的线程,重新回到updateUi代码上根据之前记录的所在线程继续执行,这里的协程框架也会帮我们把协程自动切换回主线程。
- doSomethingElse不在协程内部,所以他不会被挂起,他不会等地啊withContext里面的代码,而是直接继续执行。
上面的代码和runOnUIThread的Handler相似。
协程(协程体)本质上是一段代码结构,可以理解为Runnable,这个"Runnable"比较特殊,协程框架记录上下文(包括:代码执行位置、所处的线程、其他额外资源),在需要的时候重新唤起。协程的调度器类似于线程池一样。编译器和协程的框架帮我们做了这件事。协程的恢复,本质就是callback,他会将后面还没执行完成的那部分代码打包成一个callback,然后继续执行。callback封装类似:
kotlin
fun coroutineRequestCallback(requestResult : RequestRequest?){
requestResult?.let{
updateUI(requestResult)
}
}
requestHttp方法,在协程使用的时候加上了suspend修饰符(suspend修饰的方法只能被协程或supsend修饰的方法调用)。
suspend是kotlin引入的一个关键字,当前的方法是一个挂起函数,只有被suspend修饰的方法,他才可能被挂起(不一定会被挂起)。
挂起必要条件:
异步调用是否发生,取决于resume函数与对应的挂起函数调用是否在相同的调用栈上,切换函数调用栈的方法可以是切换到其他线程上执行,也可以不是切换线程但在当前函数返回后的某一个时刻再执行。后者其实通常是将Continuation的实例保存下来,再后续合适的时机再调用,存在事件循环的平台很容易做到。例如:Android平台主线程Looper
- resume函数与对应的挂起函数的调用是否在相同的调用栈上(resume函数代表恢复被挂起的线程,然后将结果返回)
- 切换函数调用栈的方法可以是切换到其他线程上执行
- 也可以是不切换线程但在当前函数返回之后的某一个时刻再执行。例如:Andorid里的handler。
kotlin标准的协程库使用方式:
kotlin
// 1 构造协程体,这里是挂起函数,使用suspend标记当前的函数体允许被挂起
val f : suspend () -> Int = {
log("In Coroutine.")
999
}
// 2 协程体内代码之后,进行的结果
val completion = object : Continuation<Int> {
override fun resumeWith(result: Result<Int>) {
log("Coroutine End: $result")
// 拿到协程返回的结果,这是恢复函数,也就是resume
val resultOrThrow = result.getOrThrow()
// todo
}
override val context: CoroutineContext = EmptyCoroutineContext
}
// 3 创建协程
val createCoroutine = f.createCoroutine(completion)
// 4 执行协程
createCoroutine.resume(Unit)
当调用createCoroutine.resume(Unit)时候,协程库会自动调用被suspend标记的block f里面的代码,然后将执行结果给到resumeWith(result:Result)
我们将suspend对应的函数体去掉(suspend修饰一个空函数),提示suspend modifier。 编译器推荐直接移除suspend。suspend是一个修饰符,虽然告知开发者,这个方法是一个挂起方法,但他不一定会挂起。
delay是一个可以被挂起的函数,他将会resume函数与对应的挂起函数调用放在不同的调用栈上。
delay方法被suspend修饰,且suspend是有效的,调用了suspendCancellableCoroutine
suspendCancelableCoroutine方法被suspend修饰,且suspend是有效的,调用了supendCoroutineUninterceptedOrReturn(是suspend生效),然后挂起协程的关键点。
delay、withContext、suspendCoroutine方法可以顺利挂起协程的,里面最终调用了supendCoroutineUninterceptedOrReturn方法。
kotlin协程
是一个代码结构体,类似线程池里面的Runnable、Future、以及Handler里面的Message转换出来的Runnable,只不过协程结合了kotlin编译器,封装了供开发者使用的APi的一种线程框架。协程不应该跟线程比较,如:微信小程序和微信应用一般。
协程的挂起和恢复是什么
在某一个作用域里,暂停挂起当前协程,去执行另外一个协程,当被执行的协程执行完毕后,重新去恢复被挂起的协程的执行,恢复的本质是callback,编译器帮我们做了callback的封装和调用。
suspend关键字作用
是一种结合编译器,提示开发人员,当前方法会被挂起。是否真正挂起,取决于方法体内最终是否会被调用到suspendCoroutineUninterceptedReturn方法,其中delay、withContext、suspendCoroutine都是google开发者封装好的方便挂起的API。
协程比线程效率更高、内存更小吗?
协程和线程比没有意义。协程本来就是在线程里面运行的子任务。就像线程池里面与逆行的Runnable,是HandlerThread里面发送的message对应的callback一样。如:微信小程序和微信的关系。
网上以及官方说,协程只占用4K大小,远远小于线程,没可比性。没有微信,微信小程序再小,也是无意义的。
为什么选择协程
协程并没有比线程高效。但是他提升了开发人员的开发效率、便捷、代码逻辑清晰。
优点
- 用同步方式写异步代码,避免回调地狱
- 用协程API提供的方法,非阻塞式挂起和恢复操作,实现了对长时间运行任务的优雅处理,提高开发效率
- Kotlin协程支持结构化并发,看似阻塞式写法来实现异步功能,避免并发编程常见问题,如:数据竞态和死锁。
- Kotlin协程提供如通道(Channels)、流(Flows)等高级特性,更好处理复杂的异步流程和数据流。
缺点
需要深入了解,门槛使用高,写的协程并不正确,没发挥协程优势等。