Kotlin连续性不会恢复

问题描述 投票:0回答:2

[我正试图绕开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
2个回答
2
投票

首先,您误解了suspend功能的概念。调用函数showLoginForm()不会not启动新的协程。单个协程中的代码始终按顺序执行-首先,您调用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") }
}

而且,在您的示例中,不需要暂停协程。如果我们只需在同一协程中执行其代码,为什么还需要另一个协程?我们需要等到它完成为止。由于协程按顺序执行代码,因此只有在if完成后,我们才会转到showLoginForm()分支之后的代码:

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
}

此方法最适合您的示例,因为所有代码都是顺序的。

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


1
投票

绕过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)
}

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

编辑:

试图将延续性传递给另一个活动会特别混乱。 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
        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, null)
    // respond to login state here
}

只要您使用片段的lifecycleScope而不是活动的lifecycleScope,我认为您应该免受屏幕旋转的影响。但是我自己还没有这样做。

© www.soinside.com 2019 - 2024. All rights reserved.