Kotlin 延续不会恢复
Kotlin continuation doesn't resume
我正在努力了解 suspendCoroutine
和 suspendCancellableCoroutine
。我认为它们在以下情况下可能会有用:
- 协程启动时,检查用户是否登录。
- 如果不是,请询问凭据并暂停当前正在执行的协程。
- 提交凭据后,从暂停的同一行恢复协程。
这会编译但永远不会超过 "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()
,它会延迟,不会恢复任何延续,因为 loginContinuation
是 null
,然后 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
的线程可能会观察到 loginContinuation
是 null
.
传递 Continuations 很麻烦,很容易导致您遇到的错误...一个函数在将延续分配给延续之前就完成了 属性。
既然登录表单是你想要变成暂停功能的地方,那就是你应该使用的地方suspendCoroutine
。 suspendCoroutine
是低级代码,您应该将其放在尽可能低的位置,以便您的主程序逻辑可以使用易于阅读的顺序协程,而无需嵌套 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
,我认为您应该免受屏幕旋转的影响。但是我自己没有做过。
我正在努力了解 suspendCoroutine
和 suspendCancellableCoroutine
。我认为它们在以下情况下可能会有用:
- 协程启动时,检查用户是否登录。
- 如果不是,请询问凭据并暂停当前正在执行的协程。
- 提交凭据后,从暂停的同一行恢复协程。
这会编译但永远不会超过 "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()
,它会延迟,不会恢复任何延续,因为 loginContinuation
是 null
,然后 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
的线程可能会观察到 loginContinuation
是 null
.
传递 Continuations 很麻烦,很容易导致您遇到的错误...一个函数在将延续分配给延续之前就完成了 属性。
既然登录表单是你想要变成暂停功能的地方,那就是你应该使用的地方suspendCoroutine
。 suspendCoroutine
是低级代码,您应该将其放在尽可能低的位置,以便您的主程序逻辑可以使用易于阅读的顺序协程,而无需嵌套 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
,我认为您应该免受屏幕旋转的影响。但是我自己没有做过。