Kotlin 延续不会恢复

Kotlin continuation doesn't resume

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

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

这会编译但永远不会超过 "delay over",即继续不会继续:

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"),等等。我从互联网上得到的印象是这是一个有效的用例。我做错了什么?

首先,你误解了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 .

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

既然登录表单是你想要变成暂停功能的地方,那就是你应该使用的地方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 甚至不推荐在应用中使用多个 Activity,因为很难在它们之间传递对象。要使用 Fragments 做到这一点,您可以编写您的 LoginFragment class 来拥有一个私人延续 属性,如下所示:

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
}

只要您使用片段的 lifecycleScope 而不是 Activity 的 lifecycleScope 并且第一个片段也使用 retainInstance = true,我认为您应该免受屏幕旋转的影响。但是我自己没有做过。