Я пытаюсь понять выполнение асинхронного кода с помощью сопрограмм Kotlin.
Почему я должен указывать диспетчер при запуске асинхронных блоков кода?
Следующий блок кода
fun doInParallel(): Unit = runBlocking {
coroutineScope {
launch { println("start A").also { Thread.sleep(1_000) }.also { println("finish A") } }
launch { println("start B").also { Thread.sleep(1_000) }.also { println("finish B") } }
}
}
всегда будет печатать
start A
finish A
start B
finish B
чего я не ожидал.
Только после явного указания диспетчера блоки асинхронного кода запускаются параллельно:
fun doInParallel(): Unit = runBlocking {
coroutineScope {
launch(Dispatchers.Default) { println("start A").also { Thread.sleep(1_000) }.also { println("finish A") } }
launch(Dispatchers.Default) { println("start B").also { Thread.sleep(1_000) }.also { println("finish B") } }
}
}
будет печатать
start A
start B
finish A
finish B
Почему я должен включать Dispatcher.Default
?
Я использую org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-Beta
(если это имеет какое-либо значение)
Вместо Thread.sleep(...)
используйте delay(...)
.
Thread.sleep
блокирует поток, а с сопрограммами идея состоит в том, что вы хотите приостановить поток, а не заблокировать его. Когда поток приостанавливается от одной сопрограммы с помощью функции приостановки, такой как delay
, этот же поток может свободно обслуживать другую сопрограмму — в данном случае ту, которая была запущена второй launch
.
Причина, по которой диспетчер во втором примере имеет другое поведение, заключается в том, что coroutineScope
наследует контекст сопрограммы от родительской области, которой в данном случае является runBlocking
. Диспетчер по умолчанию в конструкторе сопрограмм runBlocking
задокументирован:
CoroutineDispatcher по умолчанию для этого компоновщика является внутренней реализацией цикла событий, который обрабатывает продолжения в этом заблокированном потоке до завершения этой сопрограммы.
Другими словами, в вашем примере все выполняется внутри одного потока: основного потока (или любого другого потока, вызывающего doInParallel
). Это в сочетании с использованием блокировки Thread.sleep
означает, что код выполняется блокирующим образом и синхронно.
Обобщить:
Измените свой код, чтобы напечатать имя потока, и вы увидите, что все выполняется в потоке main
(если вы не укажете диспетчер).
Измените использование Thread.sleep
на delay
, чтобы исправить основную проблему и разрешить одному потоку запускать обе запущенные сопрограммы.
fun doInParallel(): Unit = runBlocking {
coroutineScope {
launch { println("${Thread.currentThread().name} start A").also { delay(1_000) }.also { println("${Thread.currentThread().name} finish A") } }
launch { println("${Thread.currentThread().name} start B").also { delay(1_000) }.also { println("${Thread.currentThread().name} finish B") } }
}
}
Чтобы запустить блокирующий код внутри сопрограммы, используйте Dispatchers.IO , который специально разработан для выгрузки блокирующих задач в общий пул потоков. Кроме того, согласно комментарию @George Leung, метод runInterruptible можно использовать для прерываемого вызова указанного блока, если базовый код реагирует на прерывания потока.
Они не используют механизм прерывания потока Java, и да.
По второму вопросу да, абсолютно. Хотя вы можете запустить блокирующий код внутри разработанного для этого диспетчера, например Dispatchers.IO
.
Если у вас есть блокирующий код, который можно прервать, вы можете использовать runInterruptible(Dispatchers.IO). Это преобразует отмену сопрограммы в прерывание потока.
Таким образом, сопрограммы в Kotlin нельзя прервать, а отдельные сопрограммы склонны к голоданию, если другие не работают хорошо, верно?