[Программирование, Разработка мобильных приложений, Разработка под Android, Kotlin] Как заблокировать приложение с помощью runBlocking
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Когда мы начинаем изучать корутины, то «идём» и пробуем что-то простое с билдером runBlocking, поэтому многим он хорошо знаком. runBlocking запускает новую корутину, блокирует текущий поток и ждёт пока выполнится блок кода. Кажется, всё просто и понятно. Но что, если я скажу, что в runBlocking есть одна любопытная вещь, которая может заблокировать не только текущий поток, а вообще всё ваше приложение навсегда?Напишите где-нибудь в UI потоке (например в методе onStart) такой код:
//где-то в UI потоке
runBlocking(Dispatchers.Main) {
println(“Hello, World!”)
}
Вы получите дедлок — приложение зависнет. Это не ошибка, а на 100% ожидаемое поведение. Тезис может показаться неочевидным и неявным, поэтому давайте погрузимся поглубже и я расскажу, что здесь происходит.Сравним код выше с более низкоуровневым подходом с потоками. Вы можете написать в главном потоке вот так:
//где-то в UI потоке
Handler().post {
println("Hello, World!") // отработает в UI потоке
}
Или даже так:
//где-то в UI потоке
runOnUiThread {
println("Hello, World!") // и это тоже отработает в UI потоке
}
Вроде конструкция очень похожа на наш проблемный код, но здесь обе части кода работают (по-разному под капотом, но работают). Чем они отличаются от кода с runBlocking?Как работает runBlockingДля начала небольшой дисклеймер. runBlocking редко используется в продакшн коде Android-приложения. Обычно он предназначен для использования в синхронном коде, вроде функций main или unit-тестах. Несмотря на это, мы всё-таки рассмотрим этот билдер при вызове в главном потоке Android-приложения потому, что:
- Это наглядно. Ниже мы придем к тому, что это актуально и не только для UI-потока Android-приложения. Но для наглядности лучше всего подходит пример на UI-потоке.
- Интересно разобраться, почему всё именно так работает.
- Всё-таки иногда мы можем использовать runBlocking, пусть даже в тестовых приложениях.
Билдер runBlocking работает почти так же, как и launch: создает корутину и вызывает в ней блок кода. Но чтобы сделать вызов блокирующим runBlocking создает особую корутину под названием BlockingCoroutine, у которой есть дополнительная функция joinBlocking(). runBlocking вызывает joinBlocking() сразу же после запуска корутины. Фрагмент из runBlocking():
// runBlocking() function
// …
val coroutine = BlockingCoroutine<T>(newContext, …)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
Функция joinBlocking() использует механизм блокировки Java LockSupport для блокировки текущего потока с помощью функции park(). LockSupport — это низкоуровневый и высокопроизводительный инструмент, обычно используется для написания собственных блокировок.Кроме того, BlockingCoroutine переопределяет функцию afterCompletion(), которая вызывается после завершения работы корутины.
override fun afterCompletion(state: Any?) {
//wake up blocked thread
if (Thread.currentThread ()! = blockedThread)
LockSupport.unpark (blockedThread)
}
Эта функция просто разблокирует поток, если она была заблокирована до этого с помощью park().Как это всё работает примерно показано на схеме работы runBlocking.
Что здесь делает DispatchersХорошо, мы поняли, что делает билдер runBlocking. Но почему в одном случае он блокирует UI-поток, а в другом нет? Почему Dispatchers.Main приводит к дедлоку...
// Этот код создает дедлок
runBlocking(Dispatchers.Main) {
println(“Hello, World!”)
}
...,а Dispatchers.Default — нет?
// А этот код не создает дедлок
runBlocking(Dispatchers.Default) {
println(“Hello, World!”)
}
Для этого вспомним, что такое диспатчер и зачем он нужен.Диспатчер определяет, какой поток или потоки использует корутина для своего выполнения. Это некий «высокоуровневый аналог» Java Executor. Мы даже можем создать диспатчер из Executor’а с помощью удобного экстеншна:
public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher
Dispatchers.Default реализует класс DefaultScheduler и делегирует обработку исполняемого блока кода объекту coroutineScheduler. Его функция dispatch() выглядит так:
override fun dispatch (context: CoroutineContext, block: Runnable) =
try {
coroutineScheduler.dispatch (block)
} catch (e: RejectedExecutionException) {
//…
DefaultExecutor.dispatch(context, block)
}
Класс CoroutineScheduler отвечает за наиболее эффективное распределение обработанных корутин по потокам. Он реализует интерфейс Executor.
override fun execute(command: Runnable) = dispatch(command)
А что же делает функция CoroutineScheduler.dispatch()?
- Добавляет исполняемый блок в очередь задач. При этом существует две очереди: локальная и глобальная. Это часть механизма приоритезации внешних задач.
- Создает воркеры. Воркер — это класс, унаследованный от обычного Java Thread (в данном случае daemon thread). Здесь создаются рабочие потоки. У воркера также есть локальная и глобальная очереди, из которых он выбирает задачи и выполняет их.
- Запускает воркеры.
Теперь соединим всё, что разобрали выше про Dispatchers.Default, и напишем, что происходит в целом.
- runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().
- dispatch() запускает воркеры (под капотом Java потоки).
- BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().
- Исполняемый блок кода выполняется.
- Вызывается функция afterCompletion(), которая разблокирует текущий поток с помощью LockSupport.unpark().
Эта последовательность действий выглядит примерно так.
Перейдём к Dispatchers.MainЭто диспатчер, который создан специально для Android. Например, при использовании Dispatchers.Main фреймворк бросит исключение, если вы не добавляете зависимость:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:..*'
Перед началом разбора Dispatchers.Main стоит поговорить о HandlerContext. Это специальный класс, который добавлен в пакет coroutines для Android. Это диспатчер, который выполняет задачи с помощью Android Handler — всё просто.Dispatchers.Main создаёт HandlerContext с помощью AndroidDispatcherFactory через функцию createDispatcher().
override fun createDispatcher(…) =
HandlerContext(Looper.getMainLooper().asHandler(async = true))
И что мы тут видим? Looper.getMainLooper().asHandler() означает, что он принимает Handler главного потока Android. Получается, что Dispatchers.Main — это просто HandlerContext с Handler’ом главного потока Android.Теперь посмотрим на функцию dispatch() у HandlerContext:
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}
Он просто постит исполняемый код через Handler. В нашем случае Handler главного потока.Итого, что же происходит?
- runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().
- dispatch() отправляет исполняемый блок кода через Handler главного потока.
- BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().
- Main Looper никогда не получает сообщение с исполняемым блоком кода, потому что главный поток заблокирован.
- Из-за этого afterCompletion() никогда не вызывается.
- И из-за этого текущий поток не будет разблокирован (через unparked) в функции afterCompletion().
Эта последовательность действий выглядит примерно так.
Вот почему runBlocking с Dispatchers.Main блокирует UI-поток навсегда.
Главный поток блокируется и ждёт завершения исполняемого кода. Но он никогда не завершается, потому что Main Looper не может получить сообщение на запуск исполняемого кода. Дедлок.
Совсем простое объяснениеПомните пример с Handler().post в самом начале статьи? Там код работает и ничего не блокируется. Однако мы можем легко изменить его, чтобы он был в значительной степени похож на наш код с Dispatcher.Main, и стал ещё нагляднее. Для этого можем добавить операции parking и unparking к текущему потоку, иммитируя работу функций afterCompletion() и joinBlocking(). Код начинает работать почти так же, как с билдером runBlocking.
//где-то в UI потоке
val thread = Thread.currentThread()
Handler().post {
println("Hello, World!") // это никогда не будет вызвано
// имитируем afterCompletion()
LockSupport.unpark(thread)
}
// имитируем joinBlocking()
LockSupport.park()
Но этот «трюк» не будет работать с функцией runOnUiThread.
//где-то в UI потоке
val thread = Thread.currentThread()
runOnUiThread {
println("Hello, World!") // этот код вызовется
LockSupport.unpark(thread)
}
LockSupport.park()
Это происходит потому, что runOnUiThread использует оптимизацию, проверяя текущий поток. Если текущий поток главный, то он сразу же выполнит блок кода. В противном случае сделает post в Handler главного потока.Если всё же очень хочется использовать runBlocking в UI-потоке, то у Dispatchers.Main есть оптимизация Dispatchers.Main.immediate. Там аналогичная логика как у runOnUiThread. Поэтому этот блок кода будет работать и в UI-потоке:
//где-то в UI потоке
runBlocking(Dispatchers.Main.immediate) {
println(“Hello, World!”)
}
ВыводыВ статье я описал как «безобидный» билдер runBlocking может заморозить ваше приложение на Android. Это произойдет, если вызвать runBlocking в UI-потоке с диспатчером Dispatchers.Main. Приложение заблокируется по следующему алгоритму:
- runBlocking создаёт блокирующую корутину BlockingCoroutine.
- Dispatchers.Main отправляет на запуск исполняемый блок кода через Handler.post.
- Но BlockingCoroutine тут же заблокирует UI поток.
- Поэтому Main Looper никогда не получит сообщение с исполняемым блоком кода.
- А UI не разблокируется, потому что корутина ждёт завершения исполняемого кода.
Эта статья больше теоретическая, чем практическая. Просто потому, что runBlocking редко встречается в продакшн-коде. Но примеры с UI-потоком наглядны, потому что можно сразу заблокировать приложение и разобраться, как работает runBlocking.Но заблокировать исполнение можно не только в UI-потоке, но и с помощью других диспатчеров, если поток вызова и корутины окажется одним и тем же. В такую ситуацию можно попасть, если мы будем пытаться вызвать билдер runBlocking на том же самом потоке, что и корутина внутри него. Например, мы можем использовать newSingleThreadContext для создания нового диспатчера и результат будет тот же. Здесь UI не будет заморожен, но выполнение будет заблокировано.
val singleThreadDispatcher = newSingleThreadContext("Single Thread")
GlobalScope.launch (singleThreadDispatcher) {
runBlocking (singleThreadDispatcher) {
println("Hello, World!") // этот кусок кода опять не выполнится
}
}
Если очень надо написать runBlocking в главном потоке Android-приложения, то не используйте Dispatchers.Main. Используйте Dispatchers.Default или Dispatchers.Main.immediate в крайнем случае.Также будет интересно почитать:— Оригинал статьи на английском «How runBlocking May Surprise You».
— Как страдали iOS-ники когда выпиливали Realm.
— О том, над чем в целом мы тут работаем: монолит, монолит, опять монолит.
— Кратко об истории Open Source — просто развлечься (да и статья хорошая).
Подписывайтесь на чат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы, а также на канал Dodo Engineering, где мы постим всё, что с нами интересного происходит.
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование] Заметки о codestyle
- [Разработка веб-сайтов, Разработка мобильных приложений, Управление продуктом, Визуальное программирование] Сервис для найма продактов с нуля на Bubble: что в платформе не так и почему она все равно крутая
- [Assembler, Программирование микроконтроллеров, Разработка под Arduino, Электроника для начинающих] Управление LCD и OLED дисплеями на AVR-ассемблере
- [Алгоритмы, Программирование микроконтроллеров, Производство и разработка электроники] Бинарный поиск в микроконтроллере
- [Разработка веб-сайтов, Программирование, Сетевые технологии, Программирование микроконтроллеров] Разрабатываем web-site для микроконтроллера
- [Программирование, DevOps, Kubernetes] Круглый стол «Нужно ли разработчику знать Kubernetes» 11 февраля
- [Разработка веб-сайтов, PHP, Программирование] Tagged Unions в PHP (примерно как в Rust)
- [Программирование, Java] Вы часто используете null? А он у нас в спецификации
- [Разработка мобильных приложений, Законодательство в IT, Социальные сети и сообщества] Роскомнадзор начал тестирование мобильного приложения для подачи жалоб на запрещенный контент в соцсетях и сервисах
- [JavaScript, Программирование, Учебный процесс в IT] Решение забавной задачки на JavaScript (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_android, #_coroutines, #_runblocking, #_dedlok (дедлок), #_mnogopotochnost (многопоточность), #_kotlin, #_razrabotka_prilozhenij (разработка приложений), #_dodo, #_dodo_engineering, #_dodopizzaengineering, #_blog_kompanii_dodo_engineering (
Блог компании Dodo Engineering
), #_programmirovanie (
Программирование
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:19
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Когда мы начинаем изучать корутины, то «идём» и пробуем что-то простое с билдером runBlocking, поэтому многим он хорошо знаком. runBlocking запускает новую корутину, блокирует текущий поток и ждёт пока выполнится блок кода. Кажется, всё просто и понятно. Но что, если я скажу, что в runBlocking есть одна любопытная вещь, которая может заблокировать не только текущий поток, а вообще всё ваше приложение навсегда?Напишите где-нибудь в UI потоке (например в методе onStart) такой код: //где-то в UI потоке
runBlocking(Dispatchers.Main) { println(“Hello, World!”) } //где-то в UI потоке
Handler().post { println("Hello, World!") // отработает в UI потоке } //где-то в UI потоке
runOnUiThread { println("Hello, World!") // и это тоже отработает в UI потоке }
// runBlocking() function
// … val coroutine = BlockingCoroutine<T>(newContext, …) coroutine.start(CoroutineStart.DEFAULT, coroutine, block) return coroutine.joinBlocking() override fun afterCompletion(state: Any?) {
//wake up blocked thread if (Thread.currentThread ()! = blockedThread) LockSupport.unpark (blockedThread) } Что здесь делает DispatchersХорошо, мы поняли, что делает билдер runBlocking. Но почему в одном случае он блокирует UI-поток, а в другом нет? Почему Dispatchers.Main приводит к дедлоку... // Этот код создает дедлок
runBlocking(Dispatchers.Main) { println(“Hello, World!”) } // А этот код не создает дедлок
runBlocking(Dispatchers.Default) { println(“Hello, World!”) } public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher
override fun dispatch (context: CoroutineContext, block: Runnable) =
try { coroutineScheduler.dispatch (block) } catch (e: RejectedExecutionException) { //… DefaultExecutor.dispatch(context, block) } override fun execute(command: Runnable) = dispatch(command)
Перейдём к Dispatchers.MainЭто диспатчер, который создан специально для Android. Например, при использовании Dispatchers.Main фреймворк бросит исключение, если вы не добавляете зависимость: implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:..*'
override fun createDispatcher(…) =
HandlerContext(Looper.getMainLooper().asHandler(async = true)) override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block) }
Вот почему runBlocking с Dispatchers.Main блокирует UI-поток навсегда. Главный поток блокируется и ждёт завершения исполняемого кода. Но он никогда не завершается, потому что Main Looper не может получить сообщение на запуск исполняемого кода. Дедлок.
//где-то в UI потоке
val thread = Thread.currentThread() Handler().post { println("Hello, World!") // это никогда не будет вызвано // имитируем afterCompletion() LockSupport.unpark(thread) } // имитируем joinBlocking() LockSupport.park() //где-то в UI потоке
val thread = Thread.currentThread() runOnUiThread { println("Hello, World!") // этот код вызовется LockSupport.unpark(thread) } LockSupport.park() //где-то в UI потоке
runBlocking(Dispatchers.Main.immediate) { println(“Hello, World!”) }
val singleThreadDispatcher = newSingleThreadContext("Single Thread")
GlobalScope.launch (singleThreadDispatcher) { runBlocking (singleThreadDispatcher) { println("Hello, World!") // этот кусок кода опять не выполнится } } — Как страдали iOS-ники когда выпиливали Realm. — О том, над чем в целом мы тут работаем: монолит, монолит, опять монолит. — Кратко об истории Open Source — просто развлечься (да и статья хорошая). Подписывайтесь на чат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы, а также на канал Dodo Engineering, где мы постим всё, что с нами интересного происходит.
=========== Источник: habr.com =========== Похожие новости:
Блог компании Dodo Engineering ), #_programmirovanie ( Программирование ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:19
Часовой пояс: UTC + 5