Understanding CPU- and I/O-bound for asynchronous operations
В современных приложениях мы выносим обработку долгих операций на фоновые потоки. Знаете ли Вы какие операции надо запускать на этих пулах потоков, а самое главное почему? Почему не использовать единый пул для всех фоновых операций? Зачем в Kotlin Coroutines и RxJava есть понятия I/O-bound и CPU-bound.
Причиной этому является как I/O операции ведут себя по сравнению к CPU интенсивным операция. Чтение данных из файлов, потоков, сокетов или сети часто содержит много ожидания: подключение к источнику, дождаться ответа и только потом идет его реальная передача, которая может также приостанавливаться из-за низкой скорости или загруженности. Операции не связанные с чтение не из оперативной памяти вызывают IOwait - системный вызов, который сообщает процессору о необходимости приостановить выполнение текущего потока, пока данные станут доступны или будут успешно переданы.
Важно помнить, что потоки, выполняющие I/O операции, проводят значительную часть времени в ожидании. При маленьком пуле потоков, выделенных для таких операций у вас может быть простой приложения.
С другой стороны, если рассмотреть операции которые интенсивно используют CPU, например процессинг изображений, тяжелые расчеты и пр., то для таких операций нет блокирующих событий как IOwait и потоки приостанавливаются только на основе механизма распределения ресурсов в ОС.
Подведя итоги описанной выше разницы работы задача с IO и CPU можно понять что:
👉 CPU и I/O операции стоит выполнять в разных пулах потоков
👉 пул для CPU интенсивных операций должен опираться на количество ядер в устройстве и не превышать его
👉 пул для I/O операций может быть довольно большим
Рассмотрим пример:
1️⃣ Скачаем картинку из сети
2️⃣ Сделаем ей blur
3️⃣ Отобразим ее на устройстве
Мы имеем 3 разных пула, которы надо использовать:
1️⃣ I/O
2️⃣ CPU
3️⃣ Main
Реализация примера на корутинах:
launch(Dispatchers.IO) { val image = api.fetchImageAsync(url).await() val blurred =withContext(Dispatchers.Default) {
image.blur() } withContext(Dispatchers.Main) { displayImage(blurred) }}Реализация на RxJava:
api.fetchImageObservable(url) .subscribeOn(Schedulers.io() .observeOn(Schedulers.computation()) .map { blurImageSync(it) } .observeOn(AndroidSchedulers.mainThread()) .subscribe { displayImage(it) }В дополнение можно сказать что Coroutine выигрывают еще в том что они не блокируют потоки и позволяют во время ожидания выполняться на них других операциям, что позволяет более эффективно использовать пулл I/O.