Котлин: Какое влияние на производительность оказывает преобразование «нормальной» функции в функцию блокировки? - PullRequest
1 голос
/ 06 апреля 2019

У меня есть функция, которая выглядит следующим образом:

fun <R> map(block: (T) -> R): Result<R> { ... }

, и я хотел бы сделать приостановленную версию:

suspend fun <R> mapAsync(block: suspend (T) -> R): Result<R> { ... }

Логика в обоих телах идентична, ноодин приостанавливает, а другой нет.

Я не хочу иметь эту дублированную логику.Единственный способ, с помощью которого я нашел это, - это вызвать функцию map для функции mapAsync, а затем обернуть результат в runBlocking:

fun <R> map(block: (T) -> R): Result<R> =
    runBlocking { mapAsync { block(it) } }

Итак, у меня есть два вопроса:

  1. Существуют ли какие-либо соображения относительно производительности, связанные с принятием "нормальной" функции, передачей ее в качестве параметра suspend и последующим блокированием до получения результата?
    • Исходя из того, что я прочитал, звучит так, будто исходный поток продолжает "выполнять работу" внутри блока приостановки, пока не достигнет первой точки приостановки.Затем продолжение помещается в очередь ожидания, и исходный поток может выполнять другую работу.
    • Однако в этом случае «реальной» точки приостановки не существует, поскольку действительная функция просто (T) -> R, хотя я не знаю, может ли компилятор сказать это.
    • Я беспокоюсь, что эта установка на самом деле использует другой поток из пула, который просто уведомляет мой первый поток, чтобы он проснулся ...
  2. Есть ли лучший способ, чтобы набор функций с приостановкой и без приостановки использовал один и тот же код?

Ответы [ 2 ]

3 голосов
/ 06 апреля 2019

Вы столкнулись с печально известной проблемой " цветная функция ".Эти два мира действительно разделены, и, хотя вы можете добавить поверхностный слой, объединяющий их, вы не сможете получить его с нулевой производительностью.Это настолько фундаментально, что, даже если предположить, что ваш suspend блок на самом деле никогда не приостанавливается, а слой переноса использует это предположение и даже не использует runBlocking, вы все же заплатите цену«быть готовым приостановить».Однако цена невелика: это означает создание небольшого объекта на каждый вызов suspend fun, который содержит данные, которые обычно находятся в собственном стеке вызовов потока.В вашем случае только внешний блок является приостановленным, так что это всего лишь один из таких объектов.

runBlocking запускает сопрограмму в потоке, в котором вы ее вызвали, и она будет синхронно завершаться в том же потоке, пока не приостановит себя.Поэтому ваш случай, когда у вас будет некоторый синхронный код в блоке suspend, не будет страдать от дополнительного снижения производительности из-за координации потоков.

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

1 голос
/ 06 апреля 2019

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

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

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html

Также читайте дальше: https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/basics.md#bridging-blocking-and-non-blocking-worlds

И несколько интересных видео Романа Елизарова:

https://youtu.be/_hfBv0a09Jc

https://youtu.be/a3agLJQ6vt8

...