как работает метод резюме или диспетчер работает в сопрограммах Kotlin, что приводит к разным выходным последовательностям - PullRequest
1 голос
/ 21 марта 2020

Ниже приведены два фрагмента, они почти одинаковы, за исключением того, что у одного есть поток в сопрограмме suspend, а у другого нет.

Первый код:

suspend fun main(args: Array<String>) {
    log(0)

    coroutineScope {
        log(1)
        val result = suspendCoroutine<String> { continuation ->
            log(2)
            Thread {
                log(3)
                continuation.resume("cup")
                log(4)
            }.start()
        }
        log("5 $result")
    }

    log(6)

    Thread.sleep(100)
}

Его вывод:

    [main] 0
    [main] 1
    [main] 2
    [Thread-0] 3
    [Thread-0] 5 cup
    [Thread-0] 6
    [Thread-0] 4

и второй код:

suspend fun main(args: Array<String>) {
    log(0)

    coroutineScope {
        log(1)
        val result = suspendCoroutine<String> { continuation ->
            log(2)
            // we don’t use thread in suspend coroutine
            // Thread {
                log(3)
                continuation.resume("cup")
                log(4)
            // }.start()
        }
        log("5 $result")
    }

    log(6)

    Thread.sleep(100)
}

Его вывод:

    [main] 0
    [main] 1
    [main] 2
    [main] 3
    [main] 4
    [main] 5 cup
    [main] 6

и функция журнала:

fun <T> log(t: T) {
    println("[${Thread.currentThread().name}] ${t.toString()}")
}

Почему одна выходная последовательность 0-> 1-> 2-> 3-> 5-> 6-> 4, а другая 0-> 1-> 2-> 3-> 4-> 5-> 6。

Ответы [ 3 ]

0 голосов
/ 21 марта 2020
suspendCoroutine<String> { continuation ->
    log(2)
    log(3)
    continuation.resume("cup")
    log(4)
}

Когда вы возобновляете продолжение, находясь в вызове suspendCoroutine, никакого приостановления фактически не происходит, и suspendCoroutine просто возвращает значение, с которым вы возобновили его. Таким образом, в этом случае порядок выполнения является полностью последовательным и детерминированным c.

val result = suspendCoroutine<String> { continuation ->
    log(2)
    thread {
        log(3)
        continuation.resume("cup")
        log(4)
    }
}
log("5 $result")

Здесь приостановка фактически происходит, потому что suspendCoroutine завершается, а продолжение еще не возобновлено. Это означает, что диспетчер может свободно запускать другие сопрограммы, и за этим следует истинный параллелизм. Вы можете получить несколько разных заказов в зависимости от времени. Заказ 2-3-4-5 также возможен, как и то, что вы наблюдали.

0 голосов
/ 25 марта 2020

Первое, что следует отметить, это то, что результат будет другим с runBlocking.

fun main() = runBlocking { ...

С runBlocking сопрограмма будет ограничена событием l oop.

Затем, вызвав continuation.resume, созданный поток передаст результат сопрограмме, которая будет выполнена основным потоком. И 4 и 5 6 будут регистрироваться одновременно. Это может быть 4 5 6, 5 4 6 или 5 6 4.


suspend fun main не определяет Dispatcher, поэтому, когда Continuation возобновляется, он использует текущая тема. Нет ограничений по нитям.

https://github.com/Kotlin/kotlinx.coroutines/issues/1591#issuecomment -537421694

Так что это похоже на запуск сопрограммы в Unconfined.

При возобновлении сопрограммы могут произойти две вещи. Давайте посмотрим на определение suspendCoroutine.

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
    suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }

getOrThrow в основном и reusme в новом потоке будут участвовать в гонке для установки поля result в SafeContinuation.

Если выиграет getOrThrow, сопрограмма будет приостановлена. Новый поток выполнит остальную часть сопрограммы самостоятельно (потому что она не ограничена). Он может регистрировать 4 только после запуска продолжения.
Т.е. результат будет 5 6 4.

Если resume выиграет, getOrThrow вернет фактический результат, и сопрограмма не будет приостановлено. Новый поток не пытается запустить продолжение, потому что основной поток продолжит выполнение остальной части сопрограммы.
Снова, 4 и 5 6 будут зарегистрированы одновременно.

Вы можете сделайте resume победой sleep в главном после запуска нового потока.


Если вы посмотрели код и поняли предыдущие параграфы. Я надеюсь, теперь очевидно, почему результат всегда 4 5 6, когда вы resume синхронно внутри suspendCoroutine. Поскольку resume всегда вызывается раньше getOrThrow.


Я думаю, что можно умело использовать Kotlin сопрограммы, не понимая ничего из вышеперечисленного. Просто используйте ограниченные сопрограммы и покончите с этим.

0 голосов
/ 21 марта 2020

Поскольку нет никаких гарантий относительно порядка выполнения сопрограмм, которые выполняются параллельно.

Continuation.resume определяет следующее поведение:

Возобновляет выполнение соответствующего Передача сопрограммного значения в качестве возвращаемого значения последней точки приостановки.

Не указывается, когда и где он будет выполнять этот код. То, что вы видите, - это оптимизация, когда код выполняется непосредственно в возобновляющем потоке, если это не нарушает другие гарантии по соображениям производительности.

В качестве упражнения вы можете go просмотреть все приостановки в вашем коде и получить следующие правила, которым должен следовать любой вывод:

  1. log(0) первый
  2. log(6) после log(5).
  3. log(1) после log(0).
  4. log(2) после log(1)
  5. log(3) после log(2)
  6. log(5) после continuation.resume (Resume заставляет suspendCoroutine выдавать возвращаемое значение)
  7. log(4) после continuation.resume

Не требуется, чтобы log(4) выполнялся до log(5) или log(6) в порядке приостановленных операций, поэтому его полная реализация -dependant.

...