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

前言

  1. 协程是轻量级的线程
  2. 协程又称微线程
  3. 协程(Coroutines),是基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做用户空间线程,具有对内核来说不可见的特性。
  4. 协程,又叫纤程(Fiber),或者绿色线程(GreenThread)

协程是一种协作式的代码程序,可以让开发人员自己去进行人为控制的一种框架。一种并发理念,在Kotlin开发语言的协程还是和其他平台不同的。

协程、协作

Kotlin的协程是在线程基础上封装的线程框架。

JVM当中,线程是CPU调度的基本单位,它运行在内核态,线程的执行不能靠Java开发人员的人为的干预,即便可以提高线程的优先级,也不能保证线程一定优先执行,线程是抢占式的。(线程和协程不是同级别的)

// 定义一个线程,优先级为50,并且启动它
thread(priority = 50) { 
    Log.i(TAG,"start thread 1")
}
// 定义一个线程,优先级为100,并且启动它
thread(priority = 100) {
    Log.i(TAG,"start thread 2")
}

经过几次运行,他们的顺序是不能保证的。所以线程是抢占式的。

// 定义一个线程,优先级为50,并且启动它
thread(priority = 50) { 
    Log.i(TAG,"start thread 1")
    // 定义一个线程,优先级为100,并且启动它
    thread(priority = 100) {
        Log.i(TAG,"start thread 2")
    }
}

这样写可以保证线程的执行顺序。

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组合线程挂起和恢复。

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();
}

fun main()= runBlocking {
    ThreadExample.test()
    // 防止父线程过早退出
    Thread.sleep(5500)
}
输出:
thread1 wait
thread2 sleep 5seconds
thread2 notify
thread2 resumed

不过,java的wait实际上是会阻塞当前线程。

// 定义请求方法
fun requestHttp(
    requestParams : Map<String,String>, 
    block : (RequestResult) -> Unit
) = requestHttpReal(requestParams,block) // 这里模拟真实的请求

val requestParams = assembleRequestParams() // 这里是主线程
thread {                                    // 这里会“挂起”当前的线程
    requestHttp(requestParams){             // 这里是IO线程,进行http请求
        requestResult->{
            runOnUiThread{ updateUI(requestResult) }     // 这里会“恢复”当前的线程,切换回主线程,进行UI刷新
        }
    }
}
doSomethingElse()

看似主线程被挂起,其实主线程还在执行(下面的doSomethindelse)。

协程的挂起和恢复

在需要的方法上加上suspend关键字。

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封装类似:

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标准的协程库使用方式:

// 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)等高级特性,更好处理复杂的异步流程和数据流。

缺点

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

如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料


在这里插入图片描述
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )

  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin协程是一种轻量级的并发框架,它可以在不创建新线程的情况下实现异步操作。Kotlin协程的实现原理是基于挂起函数和Continuation(协程上下文)。 在Kotlin协程中,挂起函数是指可以被暂停执行,并在稍后继续执行的函数。在挂起函数中,可以使用`suspend`关键字来标记该函数为挂起函数。当调用一个挂起函数时,该函数会返回一个`Continuation`对象,该对象可以被用来在稍后的时间点恢复挂起函数的执行。 Kotlin协程的调度器会在适当的时候调用Continuation对象的`resume`方法来恢复挂起函数的执行。当一个挂起函数被恢复执行时,它会从上一次挂起的地方继续执行,直到函数结束或者再次遇到挂起点。 Kotlin协程的实现原理可以用以下伪代码来说明: ```kotlin fun main() { launch { println("Hello") delay(1000) println("World") } } suspend fun delay(time: Long) { // 挂起当前协程,等待一段时间 // 通过Continuation对象来恢复协程的执行 suspendCoroutine<Unit> { continuation -> Timer().schedule(time) { continuation.resume(Unit) } } } fun launch(block: suspend () -> Unit) { // 创建一个新的协程,并将其加入到调度器中 val coroutine = Coroutine(block) coroutine.start() } class Coroutine(private val block: suspend () -> Unit) { fun start() { block.startCoroutine(this) } } class Continuation(private val coroutine: Coroutine) { fun resume(value: Any?) { coroutine.resume(value) } } suspend fun <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T { // 挂起当前协程,等待Continuation对象被调用 // 通过Continuation对象来恢复协程的执行 return suspendCoroutineOrReturn { continuation -> block(Continuation(coroutine)) } } ``` 在上面的代码中,`launch`函数用于创建一个新的协程,并将其加入到调度器中。`Coroutine`类表示一个协程,`start`方法用于启动协程的执行。`suspendCoroutine`函数用于挂起当前协程,并等待Continuation对象被调用以恢复协程的执行。`delay`函数使用`suspendCoroutine`函数来实现挂起功能,并在一定时间后恢复协程的执行。 Kotlin协程的实现原理比较复杂,但是开发者只需要关注如何使用协程来实现异步操作即可。通过使用协程开发者可以编写出更加简洁、易于理解、易于维护的异步代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值