- Руководство по фоновой работе в Android. Часть 5: Корутины в Котлине
- Kotlin Flow — Implementing an Android Timer
- What’s in a timer?
- The UI
- The Flow of Logic
- Replacing Java Timer with Kotlin Coroutine Timer #1186
- Comments
- anggrayudi commented May 13, 2019 •
- JakeWharton commented May 13, 2019
- anggrayudi commented May 13, 2019
- JakeWharton commented May 13, 2019
- anggrayudi commented May 18, 2019 •
- napperley commented Mar 12, 2020
- napperley commented Mar 12, 2020 •
- Enrico2 commented Mar 12, 2020 •
- elizarov commented Mar 13, 2020
- zach-klippenstein commented Mar 13, 2020
- Корутины Kotlin: как работать асинхронно в Android
- Сравним корутины с потоком
- Корутины легкие и супербыстрые
- Как же корутина приостанавливает свою работу?
Руководство по фоновой работе в Android. Часть 5: Корутины в Котлине
Остров Котлин
Итак, этот час настал. Это статья, ради которой была написана вся серия: объяснение, как новый подход работает «под капотом». Если вы пока не знаете и того, как им пользоваться, вот для начала полезные ссылки:
- Страница на официальном сайте
- Блог-пост Android Coroutine Recipes
- Доклад Романа Елизарова
А освоившись с корутинами, вы можете задаться вопросом, что позволило Kotlin предоставить эту возможность и как она работает. Прошу заметить, что здесь речь пойдёт только о стадии компиляции: про исполнение можно написать отдельную статью.
Первое, что нам нужно понять — в рантайме вообще-то не существует никаких корутин. Компилятор превращает функцию с модификатором suspend в функцию с параметром Continuation. У этого интерфейса есть два метода:
Тип T — это тип возвращаемого значения вашей исходной suspend-функции. И вот что на самом деле происходит: эта функция выполняется в определённом потоке (терпение, до этого тоже доберёмся), и результат передаётся в resume-функцию того continuation, в контексте которого вызывалась suspend-функция. Если функция не получает результат и выбрасывает исключение, то вызывается resumeWithException, пробрасывая ошибку вызывавшему коду.
Хорошо, но откуда взялось continuation? Разумеется, из корутиновского builder! Давайте посмотрим на код, создающий любую корутину, к примеру, launch:
Тут builder создаёт корутину — экземпляр класса AbstractCoroutine, который, в свою очередь, реализует интерфейс Continuation. Метод start принадлежит интерфейсу Job. Но найти определение метода start весьма затруднительно. Но мы можем зайти тут с другой стороны. Внимательный читатель уже заметил, что первый аргумент функции launch — это CoroutineContext, и по умолчанию ему присвоено значение DefaultDispatcher. «Диспетчеры» — это классы, управляющие исполнением корутин, так что они определённо важны для понимания происходящего. Давайте посмотрим на объявление DefaultDispatcher:
Так что, по сути, это CommonPool, хотя java-доки и говорят нам, что это может измениться. А что такое CommonPool?
Это диспетчер корутин, использующий ForkJoinPool в качестве реализации ExecutorService. Да, это так: в конечном счёте все ваши лямбда-корутины — это просто Runnable, попавшие в Executor с набором хитрых трансформаций. Но дьявол как всегда в мелочах.
Fork? Или join?
Судя по результатам опроса в моём твиттере, тут требуется вкратце объяснить, что представляет собой FJP 🙂
While I am working on the 5th part of the «#Android background in a nutshell», I must ask you: how familiar you are with ForkJoinPool? RT for bigger audience
В первую очередь, ForkJoinPool — это современный executor, созданный для использования с параллельными стримами Java 8. Оригинальная задача была в эффективном параллелизме при работе со Stream API, что по сути означает разделение потоков для обработки части данных и последующее объединение, когда все данные обработы. Упрощая, представим, что у вас есть следующий код:
Сумма такого стрима не будет вычислена в одном потоке, вместо этого ForkJoinPool рекурсивно разобьёт диапазон на части (сначала на две части по 500 000, затем каждую из них на 250 000, и так далее), посчитает сумму каждой части, и объединит результаты в единую сумму. Вот визуализация такого процесса:
Потоки разделяются для разных задач и вновь объединяются после завершения
Эффективность FJP основана на алгоритме «похищения работы»: когда у конкретного потока кончаются задачи, он отправляется в очереди других потоков пула и похищает их задачи. Для лучшего понимания можно посмотреть доклад Алексея Шипилёва или проглядеть презентацию.
Отлично, мы поняли, что выполняет наши корутины! Но как они там оказываются?
Это происходит внутри метода CommonPool#dispatch:
Метод dispatch вызывается из метода resume (Value: T) в DispatchedContinuation. Звучит знакомо! Мы помним, что Continuation — это интерфейс, реализованный в AbstractCoroutine. Но как они связаны?
Трюк заключён внутри класса CoroutineDispatcher. Он реализует интерфейс ContinuationInterceptor следующим образом:
Видите? Вы предоставляете в builder корутин простой блок. Вам не требуется реализовывать никакие интерфейсы, о которых вы знать ничего не хотите. Это всё берёт на себя библиотека корутин. Она
перехватывает исполнение, заменяет continuation на DispatchedContinuation, и отправляет его в executor, который гарантирует наиболее эффективное выполнение вашего кода.
Теперь единственное, с чем нам осталось разобраться — как dispatch вызывается из метода start. Давайте восполним этот пробел. Метод resume вызывается из startCoroutine в extension-функции блока:
А startCoroutine, в свою очередь, вызывается оператором «()» в перечислении CoroutineStart. Ваш builder принимает его вторым параметром, и по умолчанию это CoroutineStart.DEFAULT. Вот и всё!
Вот по какой причине меня восхищает подход корутин: это не только эффектный синтаксис, но и гениальная реализация.
А тем, кто дочитал до конца, достаётся эксклюзив: видеозапись моего доклада «Скрипач не нужен: отказываемся от RxJava в пользу корутин в Котлине» с конференции Mobius. Наслаждайтесь 🙂
Источник
Kotlin Flow — Implementing an Android Timer
How complex could a timer be? We’re about to find out in this dive into understanding Kotlin Flows by implementing one.
We’ll be building out the logic with Kotlin Flows in a ViewModel and showing the timer with a Jetpack Compose, Composable. State will be represented with StateFlow.
What’s in a timer?
- You have an initial state, when the timer’s inactive.
- You have it counting down after it’s tapped.
- It resets when it’s done.
If you want a jump start by looking at the code here it is.
The UI
The UI part of the timer will be represented by a CircularProgressIndicator and a Text that shows the value of the countdown numerically. The timer only starts when it’s tapped and another tap resets it.
Here’s the UI code.
Exit fullscreen mode
TimerState is a helper class. All it contains is the progress percentage (When 30 seconds elapses on a 60 second timer, that’s 0.5% progress for the CircularProgressIndicator), and the text to show for seconds remaining.
With TimerState, you can provide the remaining seconds and total seconds and it calculates the rest of the information for the Composable.
Here’s the code for TimerState
Exit fullscreen mode
There are some nuances about it you can read or skip. The nuances are, what do you show when the timer is stopped? For this data class, stopped is represented by the int for remaining seconds being null. By providing only secondsRemaining and totalSeconds the rest of the information which our TimerDisplay needs is calculated.
The Flow of Logic
I’m going to encapsulate the logic for the timer in a class called a TimerUseCase.
Here’s how it’s going to work.
Exit fullscreen mode
Effectively creates a list of numbers from the total number of seconds to 0 and emits them one by one as a Flow.
If totalSeconds was 5, we’d get 5,4,3,2,1,0 emitted.
In the final code we’d subtract this by 1 but we’ll see why in a bit. Psst, it’s related on the onStart.
Exit fullscreen mode
Means whenever an item is emitted from this Flow, first we’ll wait for 1 second and then let it proceed down the chain.
This is how the ticking of the timer is implemented.
Exit fullscreen mode
This could’ve just been the next in the chain however there’s a problem if we write it that way.
Here the only thing we’re doing is creating a TimeState but if there was a more complex operation to be performed, it could take several milliseconds and now we’re forcing time drift in the flow chain.
Here’s an example. If it takes 1 second to emit the next remaining second but another 200ms to create an object like TimeState, then 1200ms have passed before the next item can be emitted. If this cycle repeats many times over the timer wouldn’t be accurate anymore.
So we need something in between. Here’s the actual code with ‘conflate’ being used to run the transform function concurrently (at the same time) on a separate thread from the one that ticks for time.
Also if the code was left as it was, you’d only see the timer begin to tick one second after you tapped it. We want it immediately showing the full time and then begin to tick so we make two modifications.
- When the Flow is engaged, we immediately emit the total seconds as the first value on the countdown. Which means emitting totalSeconds with onStart.
Exit fullscreen mode
Then, the flow actually emits its first delayed value. The one for the next second. That’s why the flow starts with
Источник
Replacing Java Timer with Kotlin Coroutine Timer #1186
Comments
anggrayudi commented May 13, 2019 •
I often using this method to replace java.util.Timer in my projects:
java.util.Timer is buggy. On some Android devices, it causes the following crash, even though I re-init the object:
With Kotlin Coroutines, it is possible for me to prevent this terrible error. I hope you add the method I wrote above.
The text was updated successfully, but these errors were encountered:
JakeWharton commented May 13, 2019
You probably want this to be a factory for a Flow that emits either Unit or a counter on each tick.
anggrayudi commented May 13, 2019
@JakeWharton, I am sure that somebody else needs it as well. I hope this will be added to Coroutine. It is simple code, but powerful.
JakeWharton commented May 13, 2019
I would be against it as is, if that wasn’t clear. Having a Flow factory which is a timer, however, makes sense.
anggrayudi commented May 18, 2019 •
@JakeWharton I am not sure whether this is related to Coroutine itself since it is an additional method to replace Java Timer. In Java Timer, I write the following code to start a repeated task every 1 second:
The main backward from Java Timer is, we cannot start the repeated action immediately by setting the delay time to 0ms. Hence, the following approach with Coroutine is possible:
Another backward from Java Timer is, it sometimes throws java.lang.IllegalStateException: Timer was canceled and causes app to crash. I tried to re-init the Timer and start a new one. But the error still appears. I don’t know why. It affects less than 2% of my users.
I see Crashlytics reported this message. When I migrated to Coroutine timer, the error is gone. I hope you consider adding this method to Coroutine. Thanks in advance.
napperley commented Mar 12, 2020
If users want a concurrency model agnostic version of this functionality then they should down vote the comment that was made earlier.
napperley commented Mar 12, 2020 •
What Jake is proposing is to make this functionality only available if using the RX concurrency model via the Flow implementation. Doing so would be a extremely bad design decision that promotes tight coupling with the functionality only being available in Flow, even though this functionality isn’t dependent on any concurrency model.
Enrico2 commented Mar 12, 2020 •
(edited, after a PR review)
Based on the original suggestion, took a stab at a more testable variation:
elizarov commented Mar 13, 2020
@anggrayudi Can you, please, elaborate a bit on how you use a function like that. You write in your message this example:
Do what are those «some actions» that are inside? What do use it for? What is your use-case? Can you give a larger example of a piece of code that is using startCoroutineTimer ?
zach-klippenstein commented Mar 13, 2020
this functionality isn’t dependent on any concurrency model.
Callbacks are just another concurrency model. It’s trivial to convert between callbacks and Flows, but the latter already answers questions that aren’t answered by the originally proposed solution.
How would you manage the lifecycle of this timer? How do you customize the dispatcher? The original proposal uses GlobalScope and provides no mechanism to stop/dispose of the timer, which means every timer that gets created will be leaked. It also doesn’t provide a mechanism for controlling which dispatcher is used to run the timer, which can be inefficient because it forces consumers using the timer from other dispatchers to change threads just to run the delay (eg if I’m running a timer on a UI thread, I can just use the UI system’s scheduling mechanism and not hop into the default pool and back).
The advantage of exposing this functionality using Flow is that the Flow API already has answers for those questions, and doesn’t require defining a whole new API surface, with its own documentation, learning curve, etc.
I don’t understand your aversion to using Flow as the primitive here. Flow is a lightweight abstraction on basic coroutine concepts, so it’s useable from any concurrency model. It’s built-in to the coroutines library, so there’s no added dependency weight.
Источник
Корутины Kotlin: как работать асинхронно в Android
May 6, 2020 · 6 min read
Kotlin предоставляет корутины, которые помогают писать асинхронный код синхронно. Android — это однопоточная платформа, и по умолчанию все работает на основном потоке (потоке UI). Когда настает время запускать операции, несвязанные с UI (например, сетевой вызов, работа с БД, I/O операции или прием задачи в любой момент), мы распределяем задачи по различным потокам и, если нужно, передаем результат обратно в поток UI.
Android имеет свои механизмы для выполн е ния задач в другом потоке, такие как AsyncTask, Handler, Services и т.д. Эти механизмы включают обратные вызовы, методы post и другие приемы для передачи результата между потоками, но было бы лучше, если бы можно было писать асинхронный код так же, как синхронный.
С корутиной код выглядит легче. Нам не нужно использовать обратный вызов, и следующая строка будет выполнена, как только придет ответ. Можно подумать, что вызов функции из основного потока заблокирует её к тому времени, как ответ вернется, но с корутиной все иначе. Она не будет блокировать поток Main или любой другой, но все еще может выполнять код синхронно. Подробнее.
Сравним корутины с потоком
В примере выше допустим, что мы создаем новый поток, и после завершения задачи передаем результат в поток UI. Для того, чтобы сделать это правильно, нужно понять и решить несколько проблем. Вот список некоторых:
1. Передача данных из одного потока в другой — это головная боль. Так еще и грязная. Нам постоянно нужно использовать обратные вызовы или какой-нибудь механизм уведомления.
2. Потоки стоят дорого. Их создание и остановка обходится дорого, включает в себя создание собственного стека. Потоки управляются ОС. Планировщик потоков добавляет дополнительную нагрузку.
3. Потоки блокируются. Если вы выполняете такую простую задачу, как задержка выполнения на секунду (Sleep), поток будет заблокирован и не может быть использован для другой операции.
4. Потоки не знают о жизненном цикле. Они не знают о компонентах Lifecycle (Activity, Fragment, ViewModel). Поток будет работать, даже если компонент UI будет уничтожен, что требует от нас разобраться с очисткой и утечкой памяти.
Как будет выглядеть ваш код с большим количеством потоков, Async и т.д.? Мы можем столкнуться с большим количеством обратных вызовов, методов обработки жизненного цикла, передач данных из одного места в другое, что затруднит их чтение. В целом, мы потратили бы больше времени на устранение проблем, а не на логику программы.
Отметим , что это не просто другой способ асинхронного программирования , это другая парадигма.
Корутины легкие и супербыстрые
Пусть код скажет за себя.
Я создам 10к потоков, что вообще нереалистично, но для понимания эффекта корутин пример очень наглядный:
Здесь каждый поток ожидает 1 мс. Запуск этой функции занял около 12,6 секунд. Теперь давайте создадим 100к корутин (в 10 раз больше) и увеличим задержку до 10 секунд (в 10000 раз больше). Не волнуйтесь про “runBlocking” или “launch” (конструкторах Coroutine).
14 секунд. Сама задержка составляет 10 секунд. Это очень быстро. Создание 100 тысяч потоков может занять целую вечность.
Если вы посмотрите на метод creating_10k_Thread(), то увидите, что существует задержка в 1 мс. Во время нее он заблокирован, т.е. ничего не может делать. Вы можете создать только определенное количество потоков в зависимости от количества ядер. Допустим, возможно создать до 8 потоков в системе. В примере мы запускаем цикл на 10000 раз. Первые 8 раз будут созданы 8 потоков, который будут работать параллельно. На 9-й итерации следующий поток не сможет быть создан, пока не будет доступного. На 1 мс поток блокируется. Затем создастся новый поток и по итогу блокировка на 1мс создает задержку. Общее время блокировки для метода составит 10000/ мс. А также будет использоваться планировщик потоков, что добавит дополнительной нагрузки.
Для creatingCoroutines() мы установили задержку в 10 сек. Корутина приостанавливается, не блокируется. Пока метод ждет 10 секунд до завершения, он может взять задачу и выполнить ее после задержки. Корутины управляются пользователем, а не ОС, что делает их быстрее. В цифрах, каждый поток имеет свой собственный стек, обычно размером 1 Мбайт. 64 Кбайт — это наименьший объем пространства стека, разрешенный для каждого потока в JVM, в то время как простая корутина в Kotlin занимает всего несколько десятков байт heap памяти.
Еще пример для лучшего понимания:
Во фрагменте 1 мы последовательно вызываем методы fun1 и fun2 в основном потоке. На 1 секунду поток будет заблокирован. Теперь рассмотрим пример с корутиной.
Во фрагменте 2 это выглядит так, как будто они работают параллельно, но это невозможно, так как оба метода выполняются одним потоком. Эти методы выполняются одновременно потому, что функция задержки не блокирует поток, она приостанавливает его. И теперь, не теряя времени, этот же поток начинает выполнять следующую задачу и возвращается к ней, как только другая приостановленная функция (задержки) вернется к нему.
Корутина может обеспечить высокий уровень параллелизма с небольшими нагрузками. Несколько потоков также могут обеспечить параллелизм, но у них есть блокировка и переключение контекста. Корутина не блокирует, а приостанавливает поток для других задач. Большое количество корутин, выполняющих маленькие задачи, эффективнее, чем планировщик, поэтому тысячи корутин работают быстрее, чем десятки потоков.
Как же корутина приостанавливает свою работу?
Если вы посмотрите на выход, то увидите, что ‘completionHandler’ выполняется после завершения ‘asyncOperation’. ‘asyncOperation’ выполняется в фоновом потоке, а ‘completionHandler’ ожидает его завершения. В ‘completionHandler’ происходит обновление textview. Давайте рассмотрим байтовый код метода ‘asyncOperation’.
Во второй строке есть новый параметр под названием ‘continuation’, добавленный к методу asyncOperation. Continuation (продолжение) — это рабочий вариант для приостановки кода. Продолжение добавляется в качестве параметра к функции, если она имеет модификатор ‘suspend’. Также он сохраняет текущее состояние программы. Думайте о нем как о передаче остальной части кода (в данном случае метода completionHandler()) внутрь оболочки Continuation. После завершения текущей задачи выполнится блок продолжения. Поэтому каждый раз, когда вы создаете функцию suspend, вы добавляете в нее параметр продолжения, который обертывает остальную часть кода из той же корутины.
Coroutine очень хорошо работает с Livedata, Room, Retrofit и т.д. Еще один пример с корутиной:
Источник