kotlin协程挂起、恢复、suspend关键字

前言

  1. 协程是轻量级的线程
  2. 协程又称微线程
  3. 协程(Coroutines),是基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做用户空间线程,具有对内核来说不可见的特性。
  4. 协程,又叫纤程(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里协程执行完耗时操作后才执行),避免回调地狱。

  1. 使用GlobalScope.launch创建协程Coroutine-1,尽管这个协程在主线程运行,但是他具备了协程的挂起和恢复的能力。
  2. 当执行withContext(Dispatchers.IO)时候,开启了第二个协程Couroutine-2
  3. 因为withContext函数的特性,它会挂起当前协程(Coroutine-1),此时会将我们的执行点进行一个上下文记录(包括协程执行的代码点,所处的线程、它的上下文数据),也就是updatUI之前,停止(协程coroutine-1)的执行,转而执行Coroutine-2里面的代码,也就是requestHttp的代码
  4. 由于withContext里指定的调度器是Dispatchers.IO,协程框架会自动进行线程的切换
  5. 等待requestHttp代码执行完毕,根据之前Coroutine-1的记录的代码点和所处的线程,重新回到updateUi代码上根据之前记录的所在线程继续执行,这里的协程框架也会帮我们把协程自动切换回主线程。
  6. 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

  1. resume函数与对应的挂起函数的调用是否在相同的调用栈上(resume函数代表恢复被挂起的线程,然后将结果返回)
  2. 切换函数调用栈的方法可以是切换到其他线程上执行
  3. 也可以是不切换线程但在当前函数返回之后的某一个时刻再执行。例如: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大小,远远小于线程,没可比性。没有微信,微信小程序再小,也是无意义的。

为什么选择协程

协程并没有比线程高效。但是他提升了开发人员的开发效率、便捷、代码逻辑清晰。

优点

  1. 用同步方式写异步代码,避免回调地狱
  2. 用协程API提供的方法,非阻塞式挂起和恢复操作,实现了对长时间运行任务的优雅处理,提高开发效率
  3. Kotlin协程支持结构化并发,看似阻塞式写法来实现异步功能,避免并发编程常见问题,如:数据竞态和死锁。
  4. Kotlin协程提供如通道(Channels)、流(Flows)等高级特性,更好处理复杂的异步流程和数据流。

缺点

需要深入了解,门槛使用高,写的协程并不正确,没发挥协程优势等。

相关推荐
黄林晴7 分钟前
如何判断手机是否是纯血鸿蒙系统
android
火柴就是我14 分钟前
flutter 之真手势冲突处理
android·flutter
法的空间33 分钟前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
循环不息优化不止39 分钟前
深入解析安卓 Handle 机制
android
恋猫de小郭1 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
jctech1 小时前
这才是2025年的插件化!ComboLite 2.0:为Compose开发者带来极致“爽”感
android·开源
用户2018792831671 小时前
为何Handler的postDelayed不适合精准定时任务?
android
叽哥1 小时前
Kotlin学习第 8 课:Kotlin 进阶特性:简化代码与提升效率
android·java·kotlin
Cui晨1 小时前
Android RecyclerView展示List<View> Adapter的数据源使用View
android
氦客1 小时前
Android Doze低电耗休眠模式 与 WorkManager
android·suspend·休眠模式·workmanager·doze·低功耗模式·state_doze