Каков порядок выполнения сопрограмм?

Рассмотрим следующий код на kotlin.

val scope = CoroutineScope(Dispatchers.Main + Job())
scope.launch {
   println("inside coroutine")
}
println("outside coroutine")

Мы создаем сопрограмму в основном потоке (UI), а после сопрограммы идет некоторый код.
Я знаю, что делать это в реальном коде бессмысленно, но это всего лишь теоретический вопрос.

Учитывая, что сопрограмма выполняется в основном потоке, почему println ("вне сопрограммы") ВСЕГДА выполняется первым?
Я ожидал, что иногда я увижу первым вне сопрограммы, а в других случаях сначала внутри сопрограммы, что-то вроде двух потоков.
Кто (ОС или реализация сопрограмм) решает, что сначала запускается коэ вне сопрограммы?


person Gio    schedule 17.10.2019    source источник
comment
Dispatchers.Main работает на основном петлителе. Любая сопрограмма launched будет поставлена ​​в очередь на выполнение, поэтому она всегда будет запускаться позже, поскольку у основного цикла сначала будут другие вещи (методы жизненного цикла, обратные вызовы пользовательского интерфейса и т. Д.).   -  person Pawel    schedule 18.10.2019


Ответы (1)


Учитывая, что сопрограмма выполняется в основном потоке, почему println («внешняя сопрограмма») ВСЕГДА выполняется первым?

Представим, что вместо этого ваш код был таким:

someView.post {
   println("inside post")
}
println("outside post")

Здесь мы создаем Runnable (лямбда-выражение) и передаем его post() на некотором View. post() говорит, что Runnable будет run() в основном потоке приложения ... в конце концов. Этот Runnable помещается в рабочую очередь, которую Looper, питающий основной поток приложения, использует, и он запускается, когда этот Runnable попадает в верхнюю часть очереди (более или менее подробности более беспорядочные IIRC, но здесь не важны).

Но если вы выполняете этот код в основном потоке приложения, println("outside post") всегда будет печататься первым. Runnable помещается в очередь для последующего выполнения, но вы все еще выполняете основной поток приложения, и поэтому, даже если очередь была пустой, этот Runnable не будет выполняться, пока вы не вернете управление основным потоком приложения обратно в Android. Итак, после вызова post() выполнение продолжается с println("outside post").

Под обложками Dispatchers.Main в основном использует post() (опять же, детали более сложны, но не слишком важны для этого обсуждения). Итак, когда вы launch() сопрограмму, это лямбда-выражение ставится в очередь для выполнения в конечном приложении. Но вы уже находитесь в основном потоке приложения, поэтому выполнение продолжается в обычном режиме, и println("outside post") печатается до того, как сопрограмма получит возможность что-либо сделать.

Предположим, что вместо этого ваш код был:

val scope = CoroutineScope(Dispatchers.Main + Job())
scope.launch {
   println("inside coroutine")
}
scope.launch {
   println("inside another coroutine")
}

Теперь вы находитесь в ситуации, когда теоретически любая из этих строк может быть напечатана первой. Вы ставите в очередь оба лямбда-выражения, и диспетчер должен решить, что запускать в каком потоке в какой момент. На практике меня не удивит, если «внутренняя сопрограмма» всегда будет печататься первой, поскольку простая реализация Dispatchers.Main будет использовать упорядочение FIFO при отсутствии других ограничений (например, сопрограмма блокируется при вводе-выводе). Однако вы не должны предполагать особый порядок вызова этих двух сопрограмм.

person CommonsWare    schedule 18.10.2019
comment
Для полноты, есть также (теперь устаревшая) опция CoroutineStart.UNDISPATCHED, которая заставит запущенную сопрограмму начать выполнение немедленно и передать управление родительской сопрограмме только тогда, когда она встретит точку приостановки. Это снова детерминированное поведение, но оно показывает, что поздний запуск сопрограммы не является фундаментальным. - person Marko Topolnik; 18.10.2019