suspend初探:挂起函数真的不会阻塞UI主线程吗?

918 阅读4分钟

本文摘要:

  • 在开发中为避免ANR,往往将耗时操作放在挂起函数,那么挂起函数真的不会阻塞主线程吗?文章总结了suspend的关键点,介绍了部分概念(诸如:虚拟),浅析了suspend关键字的实现原理,对文章开头问题以三版案例进行具体分析;

suspend到底是什么?

  • 官网描述:

    • suspend用于暂停执行当前协程,并保存所有局部变量。如需调用suspend函数,只能从其他suspend函数进行调用,或通过使用协程构建器(例如 launch)来启动新的协程。
  • 优秀博客总结:链接:juejin.cn/post/690343…

    • suspend关键字本质是一个接口,持有上下文引用,具有一个回调方法,并且kotlin官方定义了一些针对suspend关键字的使用方法,通过suspend关键字修饰,自己实现具体逻辑,可采用内设的接口以及方法来实现暂停协程或者挂起协程,切换线程等等操作(也就是说只使用suspend关键字并没有暂停、挂起功能)
  • 自我总结:

    • 函数角度:

      • suspend修饰的函数称为挂起函数,挂起函数A只能在挂起函数B中调用
      • 在KT中main函数也可使用suspend关键字修饰,确保了挂起函数的全局调用能力
    • 功能角度:

      • suspend本质为回调,可通过Result实例获取结果

      • 类似 Java并发中Callable接口意义

    • 原理角度:

      • 续体 + 状态机(label + switch-case)
    • 调度角度:

      • suspend并不提供线程切换功能,其往往需要配合协程块及协程作用域实现

suspend的使用

使用suspend简化代码编写逻辑

  • 预期目标:通过案例对比体现出suspend关键字的优势

  • 主要内容:suspend修饰函数,减少了接口的调用(使用了内置的函数),以同步写法实现了异步效果

  • 不使用suspend:

    • 主要内容:需要创建接口类,实现接口方法同时采用接口回调方式操作
    • 优点:回调逻辑可自定义
    • 缺点:回调地狱,代码量大
    • 代码:
     package com.zero.jiangke
     ​
     import kotlinx.coroutines.GlobalScope
     import kotlinx.coroutines.launch
     import kotlinx.coroutines.suspendCancellableCoroutine
     import kotlin.coroutines.CoroutineContext
     ​
     ​
     data class Item(val i: String = "item")
     ​
     class KotlinCPS {
     }
     ​
     /**
      * 优点: 异步, 非阻塞
     缺点: 回调嵌套, 代码难懂
      */
     class CallbackStyle {
     ​
         fun postItem(item: Item) {
     ​
             GlobalScope.launch {
                 val token = requestToken()
             }
     ​
     //        不使用suspend,通过接口回调的方式
             requestToken(object : Callback<String> {
     ​
                 override fun onResult(token: String) {
     ​
                     createPost(token, item, object : Callback<String> {
     ​
                         override fun onResult(post: String) {
     ​
                             processPost(post)
                         }
                     })
                 }
             })
         }
     ​
         fun requestToken(callback: Callback<String>) {
             print("Start request token ...")
             Thread.sleep(1000)
             println("... finish request token")
             callback.onResult("token")
         }
     ​
         fun createPost(token: String, item: Item, callback: Callback<String>) {
             print("Start create Post ... $token, $item")
             Thread.sleep(500)
             println(" ... finish create Post")
     //        通过接口回调拿到结果
             callback.onResult("ResponsePost")
         }
     ​
         fun processPost(post: String) {
             println("process post, post=$post")
         }
     ​
     ​
         interface Callback<T> {
             fun onResult(value: T)
             fun onError(exception: Exception) {}
         }
     }
     ​
     fun main() {
     ​
     }
    
  • 使用suspend关键字

    • 优化之处:

      • 避免自定义接口以及显示回调,配合协程块以同步思维写出异步效果
    • 优点:代码量且结构清晰

    • 缺点:需要实现自定义效果,则需额外编写代码

     package com.zero.jiangke
     ​
     import kotlinx.coroutines.GlobalScope
     import kotlinx.coroutines.launch
     import kotlinx.coroutines.suspendCancellableCoroutine
     import kotlin.coroutines.CoroutineContext
     ​
     ​
     data class Item(val i: String = "item")
     ​
     class KotlinCPS {
     }
     ​
     /**
      * 优点: 异步, 非阻塞
     缺点: 回调嵌套, 代码难懂
      */
     class CallbackStyle {
     ​
         fun postItem(item: Item) {
     ​
             GlobalScope.launch {
                 val token = requestToken()
             }
     ​
         //采用suspend修饰函数
         suspend fun requestToken(): String{
            return suspendCancellableCoroutine{cont ->
                Thread{
                    print("Start request token ...")
                    Thread.sleep(1000)
                    println("... finish request token")
     //               通过协程方式拿到结果
                    cont.resumeWith(Result.success("xxxxxx"))
                }.start()
     ​
            }
         }
     ​
         fun createPost(token: String, item: Item, callback: Callback<String>) {
             print("Start create Post ... $token, $item")
             Thread.sleep(500)
             println(" ... finish create Post")
     //        通过接口回调拿到结果
             callback.onResult("ResponsePost")
         }
     ​
         fun processPost(post: String) {
             println("process post, post=$post")
         }
     }
     ​
     fun main() {
     ​
     }
    
  • 使用suspend源码部分:

     //这个是源码
     public interface Continuation<in T> {//续体 CPS
         public val context: CoroutineContext
         //        类似于Callback
         public fun resumeWith(result: Result<T>)
     }
    
    • Continuation:续体,本质上仍然是回调
  • Result

    • 函数签名:

    图片.png

    • 重要方法:内部封装了类似Callback接口

    图片.png

如何正确使用suspend关键字

  • 主要内容:以三版例子详细证明了suspend关键字并无线程切换功能
  • 问题描述:是不是使用了suspend,就变成了协程,就不会阻塞

    • 问题背景(官网描述):suspend 提供了这样一个约定(Convention):调用这个函数不会阻塞当前调用的线程

    • 实际效果:并不是这样,挂起函数同样需要处理

    • 结论:

      • 挂起函数只有在协程块包裹的协程作用域才不会进行阻塞

      • suspend本质就是回调并没有线程切换的作用,要想实现主线程中调用包含耗时操作的挂起函数耗时不阻塞主线程那么需要用户进行线程的切换

        • 显示的切换:单独编写代码
        • 隐式的切换:耗时操作放在协程块包裹的协程作用域内
第一版:仅使用挂起函数包裹耗时操作
 package com.zero.jiangke
 ​
 import kotlinx.coroutines.*
 import java.math.BigInteger
 import java.util.*
 import java.util.concurrent.CountDownLatch
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
 import kotlin.coroutines.resume
 ​
 class Test02 {
     //耗时操作:求解素数
     suspend fun foo() {
         println("foo start: ${System.currentTimeMillis()}, Thread:                                    ${Thread.currentThread().name}")
         val t = BigInteger.probablePrime(4096, Random())//耗时操作
         println("foo finish: ${System.currentTimeMillis()}, Thread:                                  ${Thread.currentThread().name}, t=$t")
     }//单纯地给函数加上 suspend 关键字并不会神奇地让函数变成非阻塞的
 }
 suspend fun main(){
     val t = Test02()
     log("1")
     t.foo()
     log("2")
 }
  • 运行结果:

    • 肉眼可见,线程1执行时会明显消耗时间(等一会儿,线程2才打印)

      图片.png

第二版例子:在挂起函数中使用协程作用域包裹耗时操作
 package com.zero.jiangke
 ​
 import kotlinx.coroutines.*
 import java.math.BigInteger
 import java.util.*
 import java.util.concurrent.CountDownLatch
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
 import kotlin.coroutines.resume
 ​
 class Test02 {
     //耗时操作:求解素数
     suspend fun foo() {
         println("foo start: ${System.currentTimeMillis()}, Thread:                                    ${Thread.currentThread().name}")
         val t = BigInteger.probablePrime(4096, Random())//耗时操作
         println("foo finish: ${System.currentTimeMillis()}, Thread:                                  ${Thread.currentThread().name}, t=$t")
     }//单纯地给函数加上 suspend 关键字并不会神奇地让函数变成非阻塞的
     
     //第二版修改:添加了作用域
     suspend fun foo1() {
         withContext(Dispatchers.IO) {
             println("foo start: ${System.currentTimeMillis()}, Thread:                                    ${Thread.currentThread().name}")
             val t = BigInteger.probablePrime(4096, Random())//耗时操作
             println("foo finish: ${System.currentTimeMillis()}, Thread:                                  ${Thread.currentThread().name}, t=$t")
         }
     }
 }
 suspend fun main(){
     val t = Test02()
     log("1")
     t.foo1()
     log("2")
 }
  • 运行结果:还是阻塞了UI主线程

    图片.png

    图片.png

第三版例子:向挂起函数添加协程块并在其中使用协程作用域包裹耗时操作
 package com.zero.jiangke
 ​
 import kotlinx.coroutines.*
 import java.math.BigInteger
 import java.util.*
 import java.util.concurrent.CountDownLatch
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
 import kotlin.coroutines.resume
 ​
 class Test02 {
     //耗时操作:求解素数
     suspend fun foo() {
         println("foo start: ${System.currentTimeMillis()}, Thread:                                    ${Thread.currentThread().name}")
         val t = BigInteger.probablePrime(4096, Random())//耗时操作
         println("foo finish: ${System.currentTimeMillis()}, Thread:                                  ${Thread.currentThread().name}, t=$t")
     }//单纯地给函数加上 suspend 关键字并不会神奇地让函数变成非阻塞的
     
     //第三版修改:使用协程方式启动
     suspend fun foo1() {
         GlobalScope.launch{
             withContext(Dispatchers.IO) {
                 println("foo start: ${System.currentTimeMillis()}, Thread:                                    ${Thread.currentThread().name}")
                 val t = BigInteger.probablePrime(4096, Random())//耗时操作
                 println("foo finish: ${System.currentTimeMillis()}, Thread:                                  ${Thread.currentThread().name}, t=$t")
             }
         }
     }
 }
 suspend fun main(){
     val t = Test02()
     log("1")
     t.foo1()
     log("2")
     //避免主线程结束
     while(true)
 }
  • 运行结果:

    • 此时不会阻塞线程

补充:suspend关键字的基本例子

  • 总结下来:suspend关键字

    • 实现效果:简化编程,以同步思维写出异步效果
    • 内部原理:通过 续体 + 状态机实现(label与switch-case)
    • 主要内容: 实现处:新建续体类并添加回调函数resumeWith 调用处:传入续体,通过result拿到返回值
 package com.zero.jiangke
 ​
 import kotlinx.coroutines.*
 import java.math.BigInteger
 import java.util.*
 import java.util.concurrent.CountDownLatch
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
 import kotlin.coroutines.resume
 /*总结下来:suspend关键字
 *   实现效果:简化编程,以同步思维写出异步效果
 *   内部原理:通过 续体 + 状态机实现(label与switch-case)
 *   简化效果:
 *       普通:
 *           实现处:新建续体类并添加回调函数resumeWith
 *           调用处:传入续体,通过result拿到返回值*/
 suspend fun foo1(): Int {
     return 1
 }
 ​
 fun foo(continuation: Continuation<Any>): Any {
     //    存在续体类
     class FooContinuation : Continuation<Any> {
         var label: Int = 0
         //  内部还是有一个回调
         override fun resumeWith(result: Result<Any>) {
             val outcome = invokeSuspend()
             if (outcome === COROUTINE_SUSPENDED) return
 //    通过resume进行返回
             continuation.resume(outcome)
         }
         //        还有
         fun invokeSuspend(): Any {
             return foo(this)
         }
 ​
         override val context: CoroutineContext = EmptyCoroutineContext
     }
 ​
     val cont = (continuation as? FooContinuation) ?: FooContinuation()
     return when (cont.label) {
         0 -> {
             val delay: suspend (Long) -> Unit = ::delay
             val df = delay as Function2<Long, Continuation<Any>, Any>
             cont.label++
             df(1000, cont)
             COROUTINE_SUSPENDED
         }
         1 -> 1 // return 1
         else -> {
         }
     }
 }
 ​
 //调用逻辑
 fun main() = runBlocking {
     val latch = CountDownLatch(1)
 //    传入续体
     foo(object : Continuation<Any> {
         override val context: CoroutineContext = EmptyCoroutineContext
 //        通过result获取值
         override fun resumeWith(result: Result<Any>) {
             println(result.getOrThrow())
             latch.countDown()
         }
     })
     latch.await()
 }

suspend关键字实现原理探究

  • 对比使用suspend修饰前后,对应的 Java代码对原理进行探究

对suspend修饰的函数反编译

  • 原始代码:

         //采用suspend修饰函数
         suspend fun requestToken(): String{
            return suspendCancellableCoroutine{cont ->
                Thread{
                    print("Start request token ...")
                    Thread.sleep(1000)
                    println("... finish request token")
     //               通过协程方式拿到结果
                    cont.resumeWith(Result.success("xxxxxx"))
                }.start()
     ​
            }
         }
    
  • 反编译后的代码:

    图片.png

  • 变化:

    • 关键技术:续体+状态机
    • 原始Kotlin代码中是没有入参的,但反编译后添加了参数Continuation
    • 最终内部进行了封装,所以suspend本质上还是回调

续体

  • 原始代码:

     suspend fun foo(){
         return 1
     }
     ​
     fun main(){
     }
    
  • 反编译后:会向挂起函数添加续体入参(Continuation)

图片.png

在main方法中调用挂起函数
  • 原始代码:

     suspend fun foo(){
         return 1
     }
     ​
     suspend fun main(){
         
     }
    
  • 查看main方法反编译结果:内部依托状态机,通过label + switch-case实现回调逻辑

     @Nullable
     public static final Object main(@NotNull Continuation var0){
         Object $continuation
             label20:{
                 xxx
             }
             ……
         switch(((<undefinedtype>)$continuation).label){
             case 0:
                 ……
             case 1::
                 ……
         }
     }