Async, withContext, обработка исключений.
Это вторая часть серии статей по многопоточность с использованием Kotlin Coroutines. Если вы не смотрели первую часть, вот ссылка . Я рекомендую вам прочитать предыдущую статью, прежде чем вы продолжите читать эту часть, в ней представлены некоторые фундаментальные концепции Coroutines. В этой части мы поговорим о async
, withContext
и обработке исключений.
Async
Так что же такое async
в сопрограммах и почему они так полезны? Вспомните launch
, о чем мы говорили в прошлой части. async
это то же самое, что и launch
, он тоже запускает сопрограмму, за исключением того, что возвращает результат. Единственная разница между async
и обычной функцией сопрограммы заключается в том, что async
запускает сопрограмму в другом потоке параллельно с основным потоком, поэтому результат, возвращаемый функцией async
, откладывается во времени до тех пор, пока сопрограмма не завершит работу и не вернет результат. Вот почему async
возвращает то, что мы называем Deferred
.
Итак, когда нам нужен результат этой сопрограммы, мы вызываем метод await()
чтобы получить значение. Этот метод является методом блокировки, это означает, что если значение готово, то оно немедленно вернется, в противном случае поток будет заблокирован, пока значение не станет доступным.
Давайте посмотрим на практике, как мы используем async
. Итак, мы напишем небольшую программу, в которой есть две suspend
функции, каждая из которых после некоторой работы возвращает целое число (для простоты примера мы просто добавим задержку), после чего выведем сумму этих двух чисел.
fun main() { runBlocking { val firstDeferred = async { getFirstNumber() } val secondDeferred = async { getSecondNumber() } println ("Выполняется обработка!") delay(500) println ("Ожидание значений") val firstValue = firstDeferred .await() val secondValue = secondDeferred.await() println ("сумма равна ${firstValue + secondValue}") } } suspend fun getFirstNumber(): Int { delay(1000) val value = Random.nextInt(100) println ("возвращается первое значение: $value") return value } suspend fun getSecondNumber(): Int { delay(2000) val value = Random.nextInt(100) println ("возвращаемое второе значение: $value") return value } /* Вывод на консоль: Выполняется обработка! Ожидание значений возвращается первое значение: 45 возвращаемое второе значение: 48 сумма 93 */
Итак, здесь у нас две функции, которые будут выполнять некоторую работу, а каждая из них будет возвращать целое число. В основной программе мы запускаем эти две функции с помощью async
. После запуска этих двух async
операций, скажем, у нас есть небольшая обработка 500 миллисекунд в нашей программе, а затем мы хотим получить значения из двух функций, чтобы напечатать их сумму. Чтобы получить значение, мы должны вызвать метод await
, чтобы заставить async
метод выдать значение. Async
приостанавливает текущий поток и выполняет работу до тех пор, пока значение не будет готово, а затем возвращает его. Попробуйте запустить этот код и посмотрите вывод и задержки между каждой печатью.
Используйте
когда у вас нет возврата значения из этой сопрограммы. Используйте launch
если вы ожидаете значение.async
withContext
withContext
это функция, которая позволяет нам переключаться между Dispatchers
сопрограммами.
Допустим, у нас есть приложение для Android, которое применяет фильтр к изображению перед его отображением. Конечно, применение фильтра — это интенсивная работа, которую мы не можем применить в UiThread. Поэтому мы должны запустить сопрограмму с помощью Dispatchers.Default
(используйте диспетчеры по умолчанию для обработки ЦП), а затем, когда изображение будет готово, мы покажем его в пользовательском интерфейсе. Теперь, чтобы отобразить изображение в потоке пользовательского интерфейса, мы должны переключиться с фонового потока, который применил фильтр к изображению, на поток пользовательского интерфейса, который будет отображать отфильтрованное изображение. Для этого мы просто вызываем функцию withContext(Dispatchers.Main(){//Тут показываем изображение}
, и этот код будет выполняться в главном диспетчере, переданном методу, withContext()
который является UiThread в Android.
Давайте рассмотрим пример, мы просто напечатаем coroutineContext
в каждой области и посмотрим, на каком диспетчере работает код.
fun main() { runBlocking { launch (Dispatchers.Default) { println ("Первый контекст - $coroutineContext") withContext(Dispatchers.IO) { println ("Второй контекст - $coroutineContext") } println ("Третий контекст - $coroutineContext" ") } } } /* Вывод на консоль: Первый контекст: [StandaloneCoroutine{Active} @d018aa4 , DefaultDispatcher]. Второй контекст: [DispatchedCoroutine{Active} @71b2215 , LimitingDispatcher@2c910d33[dispatcher = DefaultDispatcher]] Третий контекст — [StandaloneCoroutine{Active} @d018aa4 , DefaultDispatcher]. */
Для первого и третьего сообщения это один и тот же контекст, один и тот же Dispatcher (в моем случае это @d018aa4), но для второго сообщения, которое печатается внутри метода withContext, контекст здесь изменен (и диспетчер).
withContext — очень полезный метод, особенно при работе с приложением Android, когда вам необходимо обновить пользовательский интерфейс в основном потоке.
Обработка исключений
В этом разделе мы поговорим об обработке исключений в сопрограмме. Тут важно, запускаем ли мы сопрограмму launch
или async
методом. Метод launch
возвращает Job
, async
возвращает Deferred
.
launch
:
- Распространение по иерархии родитель-потомок.
- Исключение будет сгенерировано немедленно, и задания не будут выполняться.
- Используйте try/catch или обработку исключений.
async
:
- Исключения откладываются до тех пор, пока значение не будет использовано (путем вызова
await()
функции). - Если результат не используется, исключение никогда не создастся.
- Используйте try-catch в сопрограмме или
await
вызов.
Теперь попробуем обработать исключения в обоих случаях. Мы начнем с launch
, пример очень простой, мы собираемся запустить сопрограмму и выдать исключение, затем мы присоединим это задание job к основному контексту (если мы не присоединимся к job, мы не получим исключение).
fun main() { runBlocking { val job = GlobalScope. launch { println ("Вызов исключения из задания") throw IllegalStateException("Это недопустимое исключение состояния!") } job.join() } } /* Вывод на консоль: IllegalStateException! */
Как только мы запустим этот код, мы получим красивое исключение IllegalStateException! Итак, вопрос в том, как мы собираемся обрабатывать это исключение? Coroutines предлагает обработчик исключений, который мы можем создать и передать в качестве параметра методу launch
.
Во-первых, давайте создадим обработчик исключений:
val myHandler = CoroutineExceptionHandler { coroutineContext, throwable -> println ("ошибка обработки: ${throwable.message}") }
Теперь мы можем добавить этот обработчик в нашу job, чтобы мы могли обрабатывать исключение.
fun main() { runBlocking { val job = GlobalScope. launch ( myHandler ) { //Передаем обработчик в функцию запуска println ("Выброс исключения из задания") throw IllegalStateException("Это недопустимое исключение состояния!") } job.join() } }
Теперь попробуйте запустить этот код, и вы увидите, что исключение обработано и программа больше не падает. А что, если мы хотим запустить сопрограмму в другом диспетчере и с определенным нами обработчиком исключений? Это очень просто, нам нужно просто добавить его в качестве параметра в методе запуска следующим образом:
val job = GlobalScope.launch(Dispatchers.IO + myHandler){//Код вставить сюда}
Итак, теперь у нас есть сопрограмма, запущенная в IO Dispatcher с определенным нами обработчиком исключений.
Теперь поговорим о async
функции. Давайте запустим async
и вызовем await
метод и посмотрим, как мы можем поймать исключение:
fun main() { runBlocking { val deferred = GlobalScope. async { println ("Вызов исключения из отложенного задания") throw IllegalStateException("Это недопустимое исключение состояния!") } deferred.await() } }
Теперь, если вы запустите этот код, вы получите исключение IllegalStateException. Чтобы перехватить исключение, мы можем использовать классический блок try-catch. У нас есть два варианта: мы можем поймать исключение при вызове await
функции или внутри самой async
функции:
fun main() { runBlocking { val deferred = GlobalScope. async { println ("Вызов исключения из deferred") throw IllegalStateException ("Это недопустимое исключение состояния!") } try { deferred.await() } catch (e: java.lang.IllegalStateException) { println ("Исключение поймано : ${e.message}") } } }