Kotlin продолжение не возобновляется - PullRequest
0 голосов
/ 18 февраля 2020

Я пытаюсь разобраться с suspendCoroutine и suspendCancellableCoroutine. Я думаю, что они могут быть полезны в следующем случае:

  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") и др. c. Когда я читаю inte rnet, у меня сложилось впечатление, что это правильный вариант использования. Что я делаю не так?

Ответы [ 2 ]

3 голосов
/ 18 февраля 2020

Прежде всего, вы неправильно понимаете концепцию 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") }
}

Кроме того, в вашем примере приостановка сопрограмм не требуется. Зачем нам нужна другая сопрограмма, если мы можем просто выполнить ее код в той же сопрограмме? Нам нужно подождать, пока все не закончится. Поскольку сопрограммы выполняют код последовательно, мы будем go кодировать после 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, может заметить, что loginContinuation равен null.

1 голос
/ 18 февраля 2020

Передача Continuations является беспорядочной и может легко привести к вашей ошибке ... одна функция завершается до того, как продолжение было назначено свойству продолжения.

Поскольку форма входа в систему - это то, что вы хотите превратиться в функцию приостановки, вот где вы должны использовать suspendCoroutine. suspendCoroutine - это низкоуровневый код, который вы должны поместить как можно ниже, чтобы логи вашей основной программы c могли использовать легко читаемые последовательные сопрограммы без вложенных вызовов 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, но на самом деле ваше взаимодействие с пользовательским интерфейсом в любом случае не будет сопрограммой с задержкой.

РЕДАКТИРОВАТЬ:

Попытка передать продолжение другому занятию была бы особенно грязной. Google даже не рекомендует использовать несколько действий в приложении, потому что трудно передавать объекты между ними. Чтобы сделать это с фрагментами, вы могли бы написать свой класс 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
}

Пока вы используете lifecycleScope фрагмента, а не lifecycleScope действия, а первый фрагмент также использует retainInstance = true, я думаю, вы должны быть защищены от поворотов экрана. Но я сам этого не сделал.

...