Kotlin协程suspend关键字理解

9,130 阅读5分钟

1、 suspend官方解释:

suspend用于暂停执行当前协程,并保存所有局部变量。如需调用suspend函数,只能从其他suspend函数进行调用,或通过使用协程构建器(例如 launch)来启动新的协程。

官方不愧是官方, 说了跟没有说一样,一头雾水

官网

2、源码看实现

例 1

在Activity直接建立一个代码示例:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d("LogUtils", "onCreate执行开始")
        var job = GlobalScope.launch(Dispatchers.Main) {
            Log.d("LogUtils", "主线程: " + Thread.currentThread())
            val asyncs = async(Dispatchers.IO) {
                Thread.sleep(50000)
                Log.d("LogUtils","子线程: " + Thread.currentThread())
                "耗时执行完毕"
            }
            Log.d("LogUtils", "执行于此")
            Log.d("LogUtils", asyncs.await())
            Log.d("LogUtils", "launch执行结束")
        }
        Log.d("LogUtils", "onCreate执行结束")
    }

运行结果:

D/LogUtils: onCreate执行开始
D/LogUtils: onCreate执行结束
D/LogUtils: 主线程: Thread[main,5,main]
D/LogUtils: 执行于此
D/LogUtils: 子线程: Thread[DefaultDispatcher-worker-1,5,main]
D/LogUtils: 耗时执行完毕
D/LogUtils: launch执行结束

上述例子结果可以看出主线程没有被阻塞,launch{···}代码块被暂停挂起最后执行在主线程中,async{···}代码块暂停被挂起执行在子线程中,asyncs.await()之后的代码被挂起最后执行在主线程中,究竟是为什么呢?

看源码实现会发现,这些挂起的执行操作被suspend关键字修饰了

如:launch{···}源代码:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

block: suspend CoroutineScope.() -> Unit接收的是launck{···}代码块并且被suspend关键字修饰,也就是{···}的代码块被定义为暂停的模块。

看到 例 1,将协程launch{···}设置为主线程操作,在执行示例代码根据log打印结果,发现launch{···}虽然设置为主线程,但是代码块并没有按照主线程顺序执行,launch{···}代码块就像是暂停,而是之后才执行并且是在主线程执行,这个过程就像是launch{···}代码块被挂起,某个时机又切回了主线程继续执行代码块的代码逻辑

难道采用suspend关键字修饰的方法或者代码块不需要而外的设定就可以实现代码挂起或者暂停,然后在某个时机再执行? (答案显然是否定的)

通过协程源码与kotlin源码看问题

kotlinx.coroutines.intrinsics.Cancellable.kt

···
internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
    receiver: R,                
    completion: Continuation<T> 
) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion).intercepted()
            .resumeCancellableWith(Result.success(Unit))
    }
···    

kotlin.coroutines.intrinsics.IntrinsicsJvm.kt (kotlin源码类)

···
public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
    receiver: R,
    completion: Continuation<T>
): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
        create(receiver, probeCompletion)
    else {
        createCoroutineFromSuspendFunction(probeCompletion) {
            (this as Function2<R, Continuation<T>, Any?>).invoke(receiver, it)//1
        }
    }
}
···
private inline fun <T> createCoroutineFromSuspendFunction(
    completion: Continuation<T>,
    crossinline block: (Continuation<T>) -> Any?
): Continuation<Unit> {
    val context = completion.context
    // label == 0 when coroutine is not started yet (initially) or label == 1 when it was
    return if (context === EmptyCoroutineContext)
        object : RestrictedContinuationImpl(completion as Continuation<Any?>) {
            private var label = 0

            override fun invokeSuspend(result: Result<Any?>): Any? =
                when (label) {
                    0 -> {
                        label = 1
                        result.getOrThrow() // Rethrow exception if trying to start with exception (will be caught by BaseContinuationImpl.resumeWith
                        block(this) // run the block, may return or suspend
                    }
                    1 -> {
                        label = 2
                        result.getOrThrow() // this is the result if the block had suspended
                    }
                    else -> error("This coroutine had already completed")
                }
        }
    else
        object : ContinuationImpl(completion as Continuation<Any?>, context) {
            private var label = 0

            override fun invokeSuspend(result: Result<Any?>): Any? =
                when (label) {
                    0 -> {
                        label = 1
                        result.getOrThrow() // Rethrow exception if trying to start with exception (will be caught by BaseContinuationImpl.resumeWith
                        block(this) // run the block, may return or suspend
                    }
                    1 -> {
                        label = 2
                        result.getOrThrow() // this is the result if the block had suspended
                    }
                    else -> error("This coroutine had already completed")
                }
        }
}

这里不研究协程源码,只拿协程源码说事,看到createCoroutineUnintercepted(receiver, completion)方法,该方法在kotlin源码中是通过suspend关键字修饰的扩展方法,看到kotlin源码的实现,在代码 //1 处代码块中,(this as Function2<R, Continuation<T>, Any?>)(this(suspend R.() -> T)对象,也就是被suspend关键字修饰的代码块)将suspend关键字修饰的对象转化为一个Function2<R, Continuation<T>, Any?>接口对象,到这里大概明白,suspend关键字修饰(suspend R.() -> T)对象实际被编译成为一个Function2<R, Continuation<T>, Any?>接口对象,而关键字suspend实际编译成了Continuation接口

这边举例来解释

例 2

class SuspendTest {

    fun test(){
        GlobalScope.launch(Dispatchers.Main) {
            Log.d("LogUtils","--------------------")
        }
    }

}

通过反编译 例 2 代码

public final class SuspendTest {
  public final void test() {
    BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)Dispatchers.getMain(), null, new SuspendTest$test$1(null), 2, null);
  }
  
  
  static final class SuspendTest$test$1 extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
    int label;
    
    private CoroutineScope p$;
    
    SuspendTest$test$1(Continuation param1Continuation) {
      super(2, param1Continuation);
    }
    
    public final Continuation<Unit> create(Object param1Object, Continuation<?> param1Continuation) {
      Intrinsics.checkParameterIsNotNull(param1Continuation, "completion");
      SuspendTest$test$1 suspendTest$test$1 = new SuspendTest$test$1(param1Continuation);
      CoroutineScope coroutineScope = (CoroutineScope)param1Object;
      suspendTest$test$1.p$ = (CoroutineScope)param1Object;
      return (Continuation<Unit>)suspendTest$test$1;
    }
    
    public final Object invoke(Object param1Object1, Object param1Object2) {
      return ((SuspendTest$test$1)create(param1Object1, (Continuation)param1Object2)).invokeSuspend(Unit.INSTANCE);
    }
    
    public final Object invokeSuspend(Object param1Object) {
      IntrinsicsKt.getCOROUTINE_SUSPENDED();
      if (this.label == 0) {
        ResultKt.throwOnFailure(param1Object);
        param1Object = this.p$;
        Log.d("LogUtils", "--------------------");
        return Unit.INSTANCE;
      } 
      throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
    }
  }
}

看到Function2接口

public interface Function2<P1, P2, R> extends Function<R> {
  R invoke(P1 paramP1, P2 paramP2);
}

看到反编译的后的代码,在代码 //1 处,suspend关键字修饰的代码块转化为Function2接口类,并且调用invoke(P1 paramP1, P2 paramP2)重载方法最终调用代码块逻辑

疑问

是不是通过suspend关键字修饰的代码块或者函数都能起到暂停、被挂起代码?

举个例子来说明

例 3

class SuspendTest {

    fun test(){
        GlobalScope.launch(Dispatchers.Main) {
            Log.d("LogUtils","launch开始")
            suspendTest()
            Log.d("LogUtils","launch结束")
        }
    }

    private suspend fun suspendTest() {
        Log.d("LogUtils","执行一个自定义suspend修饰方法")
    }

}

执行结果:

D/LogUtils: launch开始
D/LogUtils: 执行一个自定义suspend修饰方法
D/LogUtils: launch结束

通过 例3例1 执行结果对比,自定义的suspend关键字修饰方法并没有暂停、挂起的效果,也就是suspend关键字并不具备暂停、挂起代码块或者函数方法功能,而暂停、挂起代码块或者函数方法需要而外逻辑实现,kotlin协程框架中是有暂停、挂起代码块或者函数方法需要而外逻辑实现的

打个比方:suspend关键字就像钓鱼场的提供的渔具,显然渔具不是鱼,你拿到渔具不进行垂钓显然没有鱼,同理,suspend是kotlin的关键字,就像一个现成的工具,虽然被定义暂停或是挂起的意义,但是本身不具备真正的逻辑操作,而协程框架就是一个对suspend关键字具体操作过程来实现真正意义上的暂停与挂起,切换线程等等

结论

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

参考

1、Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了

2、张涛 Kotlin 笔记 17 协程 suspend 关键字