【问题标题】:Kotlin continuation doesn't resumeKotlin 延续没有恢复
【发布时间】:2020-06-02 07:25:59
【问题描述】:

我正在努力了解suspendCoroutinesuspendCancellableCoroutine。我认为它们在以下情况下可能有用:

  1. 协程启动时,检查用户是否登录。
  2. 如果没有,请询​​问凭据并暂停当前正在执行的协程。
  3. 提交凭据后,从暂停的同一行恢复协程。

这编译但永远不会超过“延迟”,即延续永远不会恢复:

import kotlinx.coroutines.*

fun main(args: Array<String>) {
    println("Hello, world!")

    runBlocking {
        launch {
            postComment()
        }
    }
}

var isLoggedIn = false
var loginContinuation: CancellableContinuation<Unit>? = null

suspend fun postComment() {
    if (!isLoggedIn) {
        showLoginForm()

        suspendCancellableCoroutine<Unit> {
            loginContinuation = it
        }
    }

    // call the api or whatever
    delay(1000)

    println("comment posted!")
}

suspend fun showLoginForm() {
    println("show login form")

    // simulate delay while user enters credentials
    delay(1000)
    println("delay over")
    isLoggedIn = true

    // resume coroutine on submit
    loginContinuation?.resume(Unit) { println("login cancelled") }
}

我已经尝试了所有我能想到的方法,包括将调用移至登录检查之外的suspendCancellableCoroutine,将showLoginForm 的内容包装在withContext(Dispatchers.IO) 中,使用coroutineScope.launch(newSingleThreadContext("MyOwnThread") 等等。我得到的印象从阅读互联网来看,这是一个有效的用例。我做错了什么?

【问题讨论】:

    标签: asynchronous kotlin kotlin-coroutines continuations


    【解决方案1】:

    首先,你误解了suspend函数的概念。调用函数showLoginForm() 不会启动一个新的协程。单个协程中的代码总是按顺序执行——首先你调用showLoginForm(),它会延迟,它不会恢复任何继续,因为loginContinuationnull,然后suspendCancellableCoroutine 会永远挂起你的协程并导致死锁。

    启动一个新的执行showLoginForm()的协程可以让你的代码工作:

    suspend fun CoroutineScope.postComment() {
        if (!isLoggedIn) {
            launch {
                showLoginForm()
            }
    
            suspendCancellableCoroutine<Unit> {
                loginContinuation = it
            }
        }
    
        // call the api or whatever
        delay(1000)
    
        println("comment posted!")
    }
    

    此代码仍然可能失败 (*),但在这种特殊情况下不会。此代码的工作版本可能如下所示:

    import kotlin.coroutines.*
    import kotlinx.coroutines.*
    
    fun main(args: Array<String>) {
        println("Hello, world!")
    
        runBlocking {
            postComment()
        }
    }
    
    var isLoggedIn = false
    
    suspend fun CoroutineScope.postComment() {
        if (!isLoggedIn) {
            suspendCancellableCoroutine<Unit> { continuation ->
                launch {
                    showLoginForm(continuation)
                }
            }
        }
        delay(1000)
        println("comment posted!")
    }
    
    suspend fun showLoginForm(continuation: CancellableContinuation<Unit>) {
        println("show login form")
        delay(1000)
        println("delay over")
        isLoggedIn = true
        continuation.resume(Unit) { println("login cancelled") }
    }
    

    此外,在您的示例中,不需要暂停协程。如果我们可以在同一个协程中执行它的代码,为什么还需要另一个协程?无论如何,我们需要等到它完成。由于协程是按顺序执行代码的,所以我们只有在showLoginForm()完成后才会去if分支之后的代码:

    var isLoggedIn = false
    
    suspend fun postComment() {
        if (!isLoggedIn) {
            showLoginForm()
        }
        delay(1000)
        println("comment posted!")
    }
    
    suspend fun showLoginForm() {
        println("show login form")
        delay(1000)
        println("delay over")
        isLoggedIn = true
    }
    

    这种方法最适合您的示例,其中所有代码都是顺序的。

    (*) - 如果在 showLoginForm 完成后调用 suspendCancellableCoroutine,则此代码仍然可能导致死锁 - 例如,如果您删除 showLoginForm 中的 delay 调用,或者如果您使用多线程调度程序 - 在 JVM 中不能保证suspendCancellableCoroutine 会早于showLoginForm 被调用。此外,loginContinuation 不是@Volatile,因此对于多线程调度程序,代码也可能因可见性问题而失败——执行showLoginForm 的线程可能会观察到loginContinuationnull

    【讨论】:

    • 我的程序是人为设计的,因为我试图了解这些东西是如何工作的。我知道 suspend 本身并没有创建协程,但我认为 launchrunBlocking 中创建了我唯一需要的协程。在suspendCancellableCoroutine 中移动launch 到底有什么变化? showLoginForm 是否需要挂起函数?
    • runBlocking 中的单个 launch 没有多大意义 - 您可以删除它,就像我在中间的 sn-p 中所做的那样,它不会改变任何东西。 launch 是您的代码只是为runBlocking 协程创建了一个子协程,因此您基本上将您的代码从父协程移动到子协程,并且父协程保持空闲直到launch 完成。
    • launch inside suspendCancellableCoroutine 实际上用不同的代码启动了一个新的协程,这将在 runBlocking 中创建的协程被挂起时工作。你不只是“移动”launch,你将它从完全无用的地方删除,然后添加到需要的地方。
    • showLoginForm 需要是一个挂起函数,当且仅当您从中调用挂起函数时。实际上,这在很大程度上取决于您使用的框架。在您的情况下,我已经说过您根本不需要创建新的协程,因为您不会异步执行任何代码 - 您只需等待事件发生。
    • 有一些将suspendCoroutine 用于基于callbacksexecutorsfutures 的API 的常见示例。 suspendCoroutine里面没有launches,因为里面的代码没有挂起。
    【解决方案2】:

    传递 Continuations 很麻烦,很容易导致您遇到的错误...一个函数在 continuation 甚至被分配给 continuation 属性之前就完成了。

    由于登录表单是您想要变成挂起功能的地方,因此您应该使用suspendCoroutinesuspendCoroutine 是低级代码,您应该将其放在尽可能低的位置,以便您的主程序逻辑可以使用易于阅读的顺序协同程序,而无需嵌套的 launch/suspendCoroutine 调用。

    var isLoggedIn = false
    
    suspend fun postComment() {
        if (!isLoggedIn) {
            showLoginForm()
        }
    
        println("is logged in: $isLoggedIn")
    
        if (isLoggedIn) {
            // call the api or whatever
            delay(1000)
            println("comment posted!")
        }
    }
    
    suspend fun showLoginForm(): Unit = suspendCancellableCoroutine { cont ->
        println("Login or leave blank to cancel:")
    
        //Simulate user login or cancel with console input
        val userInput = readLine()
        isLoggedIn = !userInput.isNullOrBlank()
        cont.resume(Unit)
    }
    

    我没有在showLoginForm() 中使用delay(),因为您不能在suspendCancellableCoroutine 块中调用挂起函数。最后三行也可以包裹在scope.launch 中并使用delay 而不是readLine,但实际上,您的UI 交互无论如何都不会是一个有延迟的协程。

    编辑:

    试图将延续传递给另一个 Activity 会特别麻烦。 Google 甚至不建议在一个应用程序中使用多个活动,因为它们之间很难传递对象。要使用 Fragments,您可以编写 LoginFragment 类,使其具有如下的私有延续属性:

    class LoginFragment(): Fragment {
    
        private val continuation: Continuation<Boolean>? = null
        private var loginComplete = false
    
        suspend fun show(manager: FragmentManager, @IdRes containerViewId: Int, tag: String? = null): Boolean = suspendCancelableCoroutine { cont ->
            continuation = cont
            retainInstance = true
            manager.beginTransaction().apply {
                replace(containerViewId, this@LoginFragment, tag)
                addToBackStack(null)
                commit()
            }
        }
    
        // Call this when login is complete:
        private fun onLoginSuccessful() {
            loginComplete = true
            activity?.fragmentManager?.popBackStack()
        }
    
        override fun onDestroy() {
            super.onDestroy()
            continuation?.resume(loginComplete)
        }
    }
    

    然后你会从另一个片段中显示这个片段,如下所示:

    lifecycleScope.launch {
        val loggedIn = LoginFragment().show(requireActivity().fragmentManager, R.id.fragContainer)
        // respond to login state here
    }
    

    只要您使用 Fragment 的 lifecycleScope 而不是 Activity 的 lifecycleScope 并且第一个 Fragment 也使用 retainInstance = true,我认为您应该避免屏幕旋转。但我自己没有这样做。

    【讨论】:

    • 我将问题中的代码减少到最低限度以演示问题。真正的程序在 Android 上运行,并在showLoginForm 中启动一个活动。我需要活动告诉我用户何时成功登录,我不能在这里使用startActivityForResult。除非我保留对延续的引用,否则如何恢复协程?
    • 查看编辑。我自己没有尝试过,但这是一个有趣的想法。
    猜你喜欢
    • 2021-12-06
    • 2017-04-26
    • 1970-01-01
    • 1970-01-01
    • 2016-10-10
    • 1970-01-01
    • 2020-02-14
    • 2012-06-26
    • 1970-01-01
    相关资源
    最近更新 更多