Почему сопрограммы Kotlin выполняются в одном потоке последовательно?

#kotlin #kotlin-coroutines

#kotlin #kotlin-сопрограммы

Вопрос:

Я думал, что вызов функции «приостановить» из контекста сопрограммы с использованием launch делает вызов асинхронным. Но в приведенном ниже примере я вижу, что 2 вызова placeOrder метода не выполняются в одном потоке один за другим. В чем моя ошибка?

 import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File

fun main() = runBlocking {
    t("1")
    launch {
        t("2")
        placeOrder("C:\Users")
        t("3")
    }
    launch {
        t("12")
        placeOrder("C:\Program Files")
        t("13")
    }
    t("4")
}


fun t(s: String) {
    val currentThread = Thread.currentThread()
    println(s   ": "   currentThread.name   " "       currentThread.id)
}

suspend fun placeOrder(d:String): String {
    t("placeOrder $d")
    val user = createUser(d) // asynchronous call to user service
    val order = createOrder(user) // asynchronous call to order service
    t("placeOrder $d finished")
    return order
}

suspend fun createUser(d:String): String {
    t("createUser $d")
    val toString = File(d).walk().map {
        it.length()
    }.sum().toString()
    t("createUser $d finished")
    return toString
}

suspend fun createOrder(user: String): String {
    t("createOrder $user")
    val toString = File("C:\User").walk().map {
        it.length()
    }.sum().toString()
    t("createOrder $user finished")
    return toString
}
  

Вывод:

 1: main 1
4: main 1
2: main 1
placeOrder C:Users: main 1
createUser C:Users: main 1
createUser C:Users finished: main 1
createOrder 1094020270277: main 1
createOrder 1094020270277 finished: main 1
placeOrder C:Users finished: main 1
3: main 1
12: main 1
placeOrder C:Program Files: main 1
createUser C:Program Files: main 1
createUser C:Program Files finished: main 1
createOrder 5651227104: main 1
createOrder 5651227104 finished: main 1
placeOrder C:Program Files finished: main 1
13: main 1
  

Комментарии:

1. Разве вы не используете два вызова launch{} ?

2. @StavroXhardha Да, я думал, что могу запустить таким образом 2 параллельных выполнения, но это не работает таким образом. Добавлен вывод к вопросу

3. Можете ли вы добавить выходные prinrln данные операторов?

4. Добавил @JohnMercier

Ответ №1:

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

 File(d).walk().map {
    it.length()
}
  

Ваши функции фактически никогда не приостанавливаются, а вместо этого они блокируют единственный поток, связанный с их runBlocking диспетчером.

Вы не дали своим сопрограммам возможности выполняться одновременно.

Если бы вы применили withContext(IO) { ... } приведенный выше код, вы получили бы параллелизм, но простого старого Java-типа, когда несколько потоков блокируются в операциях ввода-вывода вместе.

Комментарии:

1. Каковы примеры приостановленного ввода-вывода?

2. Для ввода-вывода файлов у меня нет примеров. Ввод-вывод файлов блокируется даже на низком уровне ОС. Для сетевого ввода-вывода используйте любой асинхронный API, который позволяет отправлять обратный вызов, который получает результат запроса.

3. Я использую Vertx, и у них есть http-клиент с обратным вызовом. Но как будет выглядеть такая сопрограмма, если для запуска следующей сопрограмме потребуется ее результат?

4. Вы реализуете каждый сетевой вызов как приостанавливающую функцию, следуя этой простой идиоме . После этого их составление становится тривиальным — просто вызывайте их как обычные функции и передавайте результат одной функции следующей.

Ответ №2:

Причина такого поведения двоякая:

  1. Все ваши сопрограммы выполняются в области runBlocking видимости, которая представляет собой однопоточный цикл событий. Таким образом, это означает, что когда-либо используется только один поток, если не указан другой контекст. ( launch(Dispatchers.IO) в качестве примера)
  2. Даже тогда сопрограммы могли бы чередоваться, за исключением того, что ваши сопрограммы вызывают приостанавливающие функции, которые на самом деле должны приостанавливаться. Это означает, что это фактически обычный последовательный вызов функции. Если бы ваши функции включали yield() delay(..) вызов or, вы бы увидели чередование сопрограмм при выполнении.

Комментарии:

1. Итак, каков правильный подход к написанию приостанавливаемого метода, который выполняет ввод-вывод или JDBC ot что-то вроде этого?

2. По своей природе вам нужно построить его на асинхронном API, который выполняет некоторую работу, а затем отправляет вам сообщение, когда работа выполнена, чтобы вы могли возобновить свой собственный код. JDBC по самой своей природе не может быть приостановлен (кажется, ADBA является асинхронной заменой), в то время как API, построенный на java.nio.channels.* , будет работать для файловых операций.

Ответ №3:

подпись функции запуска:

 fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job (source)
  

Согласно официальной ссылке на документацию Kotlin:

 When launch { ... } is used without parameters, it inherits the context (and
thus dispatcher) from the CoroutineScope it is being launched from.
  

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

 fun main() = runBlocking {
    t("1")
    launch(Dispatchers.Default) {
        t("2")
        placeOrder("C:\Users")
        t("3")
    }
    launch(Dispatchers.Default) {
        t("12")
        placeOrder("C:\Program Files")
        t("13")
    }
    t("4")
}
  

Что касается функций приостановки, приостанавливающая функция — это обычная функция Kotlin с дополнительным модификатором приостановки, который указывает, что функция может приостановить выполнение сопрограммы. Это не делает вызов асинхронным по умолчанию.
Вы можете выполнить код своей функции с помощью пользовательского диспетчера (например, диспетчера ввода-вывода), используя функцию Котлина withContext(), как показано ниже:

 suspend fun get(url: String) = withContext(Dispatchers.IO){/* Code for N/W logic */}
  

Это приведет к выполнению тела функции в отдельном потоке, отличном от контекста вызывающей сопрограммы.

Вот блог из 3 частей, в котором объясняется использование сопрограмм в приложениях для Android: https://medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb

Ответ №4:

заменить launch на async

читать это

Ответ №5:

По сути, приведенный выше код выполняется синхронно даже без runBlocking !

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

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