Dispatchers.IO: почему limitedParallelism не ограничивает то, что вы думаете
Приложение ходит в базу и во внешний API, оба вызова обёрнуты в withContext(Dispatchers.IO). Работает, пока API отвечает быстро. Потом на той стороне что-то ломается, ответы идут по 10 секунд — и вместе с ними встаёт локальная база, которая свободна и вообще ни при чём.
Причина в том, что Dispatchers.IO один на всё приложение. Медленный источник держит потоки ожиданием, остальные задачи стоят за ним в очереди. Разделить их может limitedParallelism, только работает эта функция не так, как обещает название.
Что такое Dispatchers.IO на самом деле
Под капотом — не отдельный пул, а представление общего планировщика, который делят IO и Default. Потоки создаются по мере надобности и гасятся, когда простаивают.
Ограничение у IO — 64 параллельные задачи или число ядер, если их больше; меняется свойством kotlinx.coroutines.io.parallelism. Число — это про задачи, а не про потоки: гарантии, что потоков будет ровно столько, никто не даёт.
У Default картина другая — параллелизм равен числу ядер, минимум два. Логика понятная: он для вычислений, а параллельно их не выполнить больше, чем есть ядер.
limitedParallelism на IO ничего не ограничивает
Название подсказывает, что вызов отрезает долю внутри тех же 64 задач. На IO всё наоборот.
val dbDispatcher = Dispatchers.IO.limitedParallelism(100)
val apiDispatcher = Dispatchers.IO.limitedParallelism(60)
В документации формулировка прямая: представления, полученные через limitedParallelism, ограничением Dispatchers.IO не связаны. На пике система может держать 64 задачи самого IO плюс 100 плюс 60 параллельно. Каждый вызов заводит себе отдельную квоту сверху, а не отрезает кусок общей.
Логика за этим такая: IO про блокирующие операции, где поток большую часть времени просто ждёт. Общий жёсткий лимит на них бессмысленен, ожидающий поток почти не стоит процессорного времени.
Расплата за удобство — легко наплодить потоков. Пять модулей, каждый со своим limitedParallelism(50), дают на пике 250 параллельных блокирующих задач. В спокойном режиме потоков будет мало: представления делят общий пул, лишние гасятся. А вот пиковую картину придётся считать самому.
На Default всё наоборот
Тот же вызов на Dispatchers.Default ведёт себя противоположно: он именно режет, создавая представление на тех же потоках, которое занимает не больше указанного числа.
val imageDispatcher = Dispatchers.Default.limitedParallelism(4)
Тяжёлая обработка не съест все ядра и не заморозит остальные вычисления. Здесь название функции описывает происходящее честно.
Разница в коде никак не видна: две внешне одинаковые строки делают противоположные вещи, и понять это можно только по тому, от какого диспетчера идёт вызов.
Как этим пользоваться
Заводите отдельный диспетчер на каждый внешний источник. База, сеть, файлы — у каждого своя квота, и медленный источник перестаёт тормозить соседей.
class ApiClient(private val http: HttpClient) {
private val dispatcher = Dispatchers.IO.limitedParallelism(20)
suspend fun fetch(url: String) = withContext(dispatcher) {
http.get(url)
}
}
Размер квоты стоит соотносить с тем, что на другом конце. Пул соединений к базе на 10 штук делает бессмысленным диспетчер на 100: 90 задач просто встанут в ожидание свободного соединения, занимая потоки впустую. Держите квоту близкой к размеру пула.
И держите в голове сумму. Каждая новая квота на IO добавляется к общему пиковому числу, а не берётся из уже выделенного.
А вы разделяете диспетчеры по источникам или ходите везде через общий Dispatchers.IO? Расскажите в комментариях, ловили ли ситуацию, когда одна медленная зависимость подвешивала всё остальное.
#mobilevkhub #kotlin
Приложение ходит в базу и во внешний API, оба вызова обёрнуты в withContext(Dispatchers.IO). Работает, пока API отвечает быстро. Потом на той стороне что-то ломается, ответы идут по 10 секунд — и вместе с ними встаёт локальная база, которая свободна и вообще ни при чём.
Причина в том, что Dispatchers.IO один на всё приложение. Медленный источник держит потоки ожиданием, остальные задачи стоят за ним в очереди. Разделить их может limitedParallelism, только работает эта функция не так, как обещает название.
Что такое Dispatchers.IO на самом деле
Под капотом — не отдельный пул, а представление общего планировщика, который делят IO и Default. Потоки создаются по мере надобности и гасятся, когда простаивают.
Ограничение у IO — 64 параллельные задачи или число ядер, если их больше; меняется свойством kotlinx.coroutines.io.parallelism. Число — это про задачи, а не про потоки: гарантии, что потоков будет ровно столько, никто не даёт.
У Default картина другая — параллелизм равен числу ядер, минимум два. Логика понятная: он для вычислений, а параллельно их не выполнить больше, чем есть ядер.
limitedParallelism на IO ничего не ограничивает
Название подсказывает, что вызов отрезает долю внутри тех же 64 задач. На IO всё наоборот.
val dbDispatcher = Dispatchers.IO.limitedParallelism(100)
val apiDispatcher = Dispatchers.IO.limitedParallelism(60)
В документации формулировка прямая: представления, полученные через limitedParallelism, ограничением Dispatchers.IO не связаны. На пике система может держать 64 задачи самого IO плюс 100 плюс 60 параллельно. Каждый вызов заводит себе отдельную квоту сверху, а не отрезает кусок общей.
Логика за этим такая: IO про блокирующие операции, где поток большую часть времени просто ждёт. Общий жёсткий лимит на них бессмысленен, ожидающий поток почти не стоит процессорного времени.
Расплата за удобство — легко наплодить потоков. Пять модулей, каждый со своим limitedParallelism(50), дают на пике 250 параллельных блокирующих задач. В спокойном режиме потоков будет мало: представления делят общий пул, лишние гасятся. А вот пиковую картину придётся считать самому.
На Default всё наоборот
Тот же вызов на Dispatchers.Default ведёт себя противоположно: он именно режет, создавая представление на тех же потоках, которое занимает не больше указанного числа.
val imageDispatcher = Dispatchers.Default.limitedParallelism(4)
Тяжёлая обработка не съест все ядра и не заморозит остальные вычисления. Здесь название функции описывает происходящее честно.
Разница в коде никак не видна: две внешне одинаковые строки делают противоположные вещи, и понять это можно только по тому, от какого диспетчера идёт вызов.
Как этим пользоваться
Заводите отдельный диспетчер на каждый внешний источник. База, сеть, файлы — у каждого своя квота, и медленный источник перестаёт тормозить соседей.
class ApiClient(private val http: HttpClient) {
private val dispatcher = Dispatchers.IO.limitedParallelism(20)
suspend fun fetch(url: String) = withContext(dispatcher) {
http.get(url)
}
}
Размер квоты стоит соотносить с тем, что на другом конце. Пул соединений к базе на 10 штук делает бессмысленным диспетчер на 100: 90 задач просто встанут в ожидание свободного соединения, занимая потоки впустую. Держите квоту близкой к размеру пула.
И держите в голове сумму. Каждая новая квота на IO добавляется к общему пиковому числу, а не берётся из уже выделенного.
А вы разделяете диспетчеры по источникам или ходите везде через общий Dispatchers.IO? Расскажите в комментариях, ловили ли ситуацию, когда одна медленная зависимость подвешивала всё остальное.
#mobilevkhub #kotlin