Kotlin Coroutines #2

Async, withContext, обработка исключений.

Это вторая часть серии статей по многопоточность с использованием Kotlin Coroutines. Если вы не смотрели первую часть, вот ссылка . Я рекомендую вам прочитать предыдущую статью, прежде чем вы продолжите читать эту часть, в ней представлены некоторые фундаментальные концепции Coroutines. В этой части мы поговорим о asyncwithContext и обработке исключений.

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}") 
        } 
    }
 }

Поделись с друзьями:
Если вам понравилась статья, подписывайтесь на наши социальные сети.

Оставьте комментарий

1 × три =