Как отменить выполнение корутины
При работе приложения может сложиться необходимость отменить выполнение корутины. Например, в мобильном приложении запущена корутина для загрузки данных с некоторого интернет-ресуса, но пользователь решил перейти к другой странице приложения, и ему больше не нужны эти данные. В этом случае чтобы зря не тратить ресурсу системы, мы можем предусмотреть отмену выполнения корутины.
Для отмены выполнения корутины у объекта Job может применяться метод cancel() :
import kotlinx.coroutines.* suspend fun main() = coroutineScope < val downloader: Job = launch< println("Начинаем загрузку файлов") for(i in 1..5)< println("Загружен файл $i") delay(500L) >> delay(800L) // установим задержку, чтобы несколько файлов загрузились println("Надоело ждать, пока все файлы загрузятся. Прерву-ка я загрузку. ") downloader.cancel() // отменяем корутину downloader.join() // ожидаем завершения корутины println("Работа программы завершена") >
В данном случае определена корутина, которая имитирует загрузку файлов. В цикле пробегаемся от 1 до 5 и условно загружаем пять файлов.
Далее вызов метода downloader.cancel() сигнализирует корутине, что надо прервать выполнение. Затем с помощью метода join() ожидаем завершения корутина, которая прервана. В итоге получим консольный вывод наподобие следующего:
Начинаем загрузку файлов Загружен файл 1 Загружен файл 2 Надоело ждать, пока все файлы загрузятся. Прерву-ка я загрузку. Работа программы завершена
Также вместо двух методов cancel() и join() можно использовать один сборный метод cancelAndJoin() :
import kotlinx.coroutines.* suspend fun main() = coroutineScope < val downloader: Job = launch< println("Начинаем загрузку файлов") for(i in 1..5)< println("Загружен файл $i") delay(500L) >> delay(800L) println("Надоело ждать, пока все файлы загрузятся. Прерву-ка я загрузку. ") downloader.cancelAndJoin() // отменяем корутину и ожидаем ее завершения println("Работа программы завершена") >
Обработка исключения CancellationException
Все suspend-функции в пакете kotlinx.coroutines являются прерываемыми (cancellable). Это значит, что они проверяют, прервана ли корутина. И если ее выполнение прервано, они генерируют исключение типа CancellationException . И в самой корутине мы можем перехватить это исключение, чтобы обработать отмену корутины. Например:
import kotlinx.coroutines.* suspend fun main() = coroutineScope < val downloader: Job = launch< try < println("Начинаем загрузку файлов") for(i in 1..5)< println("Загружен файл $i") delay(500L) >> catch (e: CancellationException ) < println("Загрузка файлов прервана") >finally < println("Загрузка завершена") >> delay(800L) println("Надоело ждать. Прерву-ка я загрузку. ") downloader.cancelAndJoin() // отменяем корутину и ожидаем ее завершения println("Работа программы завершена") >
Здесь код выполнения корутины обернут в конструкцию try . Если корутина будет прервана извне, то с помощью блока catch и перехвата исключения CancellationException мы сможем обработать отмену корутины.
И если нам надо выполнить некоторые завершающие действия, например, освободить используемые в корутине ресурсы — закрыть файлы, различные подключения к внешним ресурсам, то это можно сделать в блоке finally . Но в данном случае в этом блоке просто выводим диагностическое сообщение.
В итоге при вызове метода downloader.cancel() производейт отмена корутины. Будет сгенерировано исключение, и в корутине в блоке catch мы сможем ее обработать. В итоге получим следующий консольный вывод:
Начинаем загрузку файлов Загружен файл 1 Загружен файл 2 Надоело ждать. Прерву-ка я загрузку. Загрузка файлов прервана Загрузка завершена Работа программы завершена
Отмена выполнения async-корутины
Подобным образом можно отменять выполнение и корутин, создаваемых с помощью функции async() . В этом случае обычно вызов метода await() помещается в блок try:
import kotlinx.coroutines.* suspend fun main() = coroutineScope < // создаем и запускаем корутину val message = async < getMessage() >// отмена корутины message.cancelAndJoin() try < // ожидаем получение результата println("message: $") > catch (e:CancellationException) < println("Coroutine has been canceled") >println(«Program has finished») > suspend fun getMessage() : String
Консольный вывод программы:
Coroutine has been canceled Program has finished
Отмена корутин и тайм-ауты
В этом разделе рассматривается отмена корутин и тайм-ауты.
Отмена выполнения корутин
В долго работающем приложении вам может понадобиться детальное управление фоновыми корутинами. Например, пользователь может закрыть страницу, которая запускала корутину, из-за чего её результат больше не нужен, и её действие можно отменить. Функция launch возвращает Job , которую можно использовать для отмены запущенной корутины.
val job = launch < repeat(1000) < i ->println("job: I'm sleeping $i . ") delay(500L) > > delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancel() // cancels the job job.join() // waits for job's completion println("main: Now I can quit.")
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-01.kt). —>
Этот код выведет следующее:
job: I'm sleeping 0 . job: I'm sleeping 1 . job: I'm sleeping 2 . main: I'm tired of waiting! main: Now I can quit.
Как только главная функция вызывает job.cancel , мы больше не видим какого-либо вывода с другой корутины, потому что она была отменена. Существует также cancelAndJoin функция-расширение Job , которая объединяет вызовы cancel и join.
Отмена кооперативна
Отмена корутин кооперативна. Код корутины должен взаимодействовать, чтобы его можно было отменить. Все suspend-функции в kotlinx.coroutines — отменяемые. Они проверяют отмену корутины, и в случае отмены выбрасывают исключение CancellationException . Однако, если корутина работает над вычислениями и не проверяет на отмену, то её нельзя отменить, как это происходит, например, здесь:
val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) < var nextPrintTime = startTime var i = 0 while (i < 5) < // computation loop, just wastes CPU // print a message twice a second if (System.currentTimeMillis() >= nextPrintTime) < println("job: I'm sleeping $. ") nextPrintTime += 500L > > > delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.")
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-02.kt). —>
Запустив этот код, вы увидите как корутина продолжает выводить на экран «I’m sleeping» даже после отмены, пока job не завершится после пяти итераций.
Делаем код с вычислениями отменяемым
Есть два способа сделать вычислительный код отменяемым. Первый – периодически вызвать suspend-функцию, которая проверяет, активна ли корутина. Для этого хорошо подходит функция yield . Другой — явно проверять статус отмены. Попробуем этот подход.
val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) < var nextPrintTime = startTime var i = 0 while (isActive) < // cancellable computation loop // print a message twice a second if (System.currentTimeMillis() >= nextPrintTime) < println("job: I'm sleeping $. ") nextPrintTime += 500L > > > delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.")
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt). —>
Как вы можете увидеть, теперь цикл отменяется. isActive — это extension-параметр, доступный внутри корутины, благодаря объекту CoroutineScope .
Закрытие ресурсов при помощи finally
Отменяемые suspend-функции при отмене выбрасывают исключение CancellationException , которое может быть обработано обычным путём. Например, выражение try <. >finally <. >и Kotlin-функция use обыкновенно выполняют свои функции при завершении (отмене) корутин.
val job = launch < try < repeat(1000) < i ->println("job: I'm sleeping $i . ") delay(500L) > > finally < println("job: I'm running finally") >> delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.")
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt). —>
И join , и cancelAndJoin ожидают завершения всех финальных стадий, поэтому приведённый выше пример даёт такой вывод:
job: I'm sleeping 0 . job: I'm sleeping 1 . job: I'm sleeping 2 . main: I'm tired of waiting! job: I'm running finally main: Now I can quit.
Запуск неотменяемого блока
Любая попытка использовать suspend-функцию в блоке finally предыдущего примера приводит к CancellationException , потому что корутина, выполняющая этот код, отменена. Обычно это не проблема, так как все нормально работающие операции закрытия (закрытие файла, отмена Job или закрытие любого вида канала) обычно не блокируются и не требуют каких-либо suspend-функций. Однако в редких случаях, когда вам нужно приостановить работу отмененной корутины, вы можете обернуть соответствующий код в withContext(NonCancellable) <. >с использованием функции withContext и контекста NonCancellable , как показано в следующем примере:
val job = launch < try < repeat(1000) < i ->println("job: I'm sleeping $i . ") delay(500L) > > finally < withContext(NonCancellable) < println("job: I'm running finally") delay(1000L) println("job: And I've just delayed for 1 sec because I'm non-cancellable") >> > delay(1300L) // delay a bit println("main: I'm tired of waiting!") job.cancelAndJoin() // cancels the job and waits for its completion println("main: Now I can quit.")
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt). —>
Тайм-аут
Самая очевидная практическая причина отменить выполнение корутины — время её выполнения превысило некоторый тайм-аут. Хотя можно вручную отслеживать обращение к соответствующему Job и запускать отдельную корутину для отмены отслеживаемой после тайм-аута, есть готовая к использованию функция withTimeout , которая делает это. Посмотрите на следующий пример:
withTimeout(1300L) < repeat(1000) < i ->println("I'm sleeping $i . ") delay(500L) > >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt). —>
Этот код выведет следующее:
I'm sleeping 0 . I'm sleeping 1 . I'm sleeping 2 . Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
Исключение TimeoutCancellationException , создаваемое withTimeout , является подклассом CancellationException . Мы никогда раньше не видели его трассировку стека, напечатанную на консоли. Это потому, что внутри отмененной корутины CancellationException считается нормальной причиной её завершения. Однако в этом примере мы использовали withTimeout прямо внутри функции main .
Поскольку отмена является лишь исключением, все ресурсы закрываются в обычном порядке. Вы можете обернуть код с тайм-аутом в блоке try <. >catch (e: TimeoutCancellationException) <. >, если вам нужно сделать какое-то дополнительное действие специально для любого тайм-аута или использовать функцию withTimeoutOrNull . Она похожа на withTimeout , но возвращает null по тайм-ауту вместо создания исключения.
val result = withTimeoutOrNull(1300L) < repeat(1000) < i ->println("I'm sleeping $i . ") delay(500L) > "Done" // will get cancelled before it produces this result > println("Result is $result")
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt). —>
Теперь ошибки при выполнении этой корутины не будет:
I'm sleeping 0 . I'm sleeping 1 . I'm sleeping 2 . Result is null
Асинхронный тайм-аут и ресурсы
Событие тайм-аута в withTimeout является асинхронным по отношению к коду, работающему в его блоке, и может произойти в любое время, даже прямо перед возвратом из блока тайм-аута. Имейте это в виду, если вы открываете или приобретаете ресурс внутри блока, который необходимо закрыть или освободить за пределами блока.
Например, здесь мы имитируем закрываемый ресурс с помощью класса Resource , который просто отслеживает, сколько раз он был создан путем увеличения счетчика acquired и уменьшения этого счетчика из его функции close . Давайте запустим много корутин с небольшим таймаутом, попробуем получить этот ресурс изнутри блока withTimeout после небольшой задержки и освободить его извне.
var acquired = 0 class Resource < init < acquired++ >// Acquire the resource fun close() < acquired-- >// Release the resource > fun main() < runBlocking < repeat(100_000) < // Launch 100K coroutines launch < val resource = withTimeout(60) < // Timeout of 60 ms delay(50) // Delay for 50 ms Resource() // Acquire a resource and return it from withTimeout block >resource.close() // Release the resource > > > // Outside of runBlocking all coroutines have completed println(acquired) // Print the number of resources still acquired >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). —>
Если вы запустите приведенный выше код, вы увидите, что он не всегда выводит ноль, хотя это может зависеть от таймингов вашей машины, вам может потребоваться настроить тайм-ауты в этом примере, чтобы действительно увидеть ненулевые значения.
Note, that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe, > since it always happens from the same main thread. More on that will be explained in the chapter > on coroutine context. —>
Обратите внимание, что увеличение и уменьшение счетчика acquired здесь из 100 000 корутин совершенно безопасно, так как это всегда происходит из одного и того же основного потока. Подробнее об этом будет рассказано в главе о контексте корутин.
Чтобы обойти эту проблему, вы можете сохранить ссылку на ресурс в переменной, а не возвращать ее из блока withTimeout .
runBlocking < repeat(100_000) < // Launch 100K coroutines launch < var resource: Resource? = null // Not acquired yet try < withTimeout(60) < // Timeout of 60 ms delay(50) // Delay for 50 ms resource = Resource() // Store a resource to the variable if acquired >// We can do something else with the resource here > finally < resource?.close() // Release the resource if it was acquired >> > > // Outside of runBlocking all coroutines have completed println(acquired) // Print the number of resources still acquired
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). —>
Этот пример всегда выводит 0, а ресурсы не утекают.
Отмена в корутинах
В разработке, как и в жизни, мы знаем, что важно избегать делать больше работы, чем необходимо, так как это может привести к потере памяти и времени автономной работы. Этот принцип применим и к корутинам. Сперва необходимо убедиться, что вы контролируете жизненный цикл корутины и завершаете его, когда она больше не нужна — это то, что представляет собой структурированный параллелизм. Читайте статью далее, чтобы узнать все тонкости отмены корутины.
Если вы предпочтете посмотреть видео на эту тему, посмотрите наш разговор с Мануэлем Виво на KotlinConf ‘ 19 об отмене и исключениях в корутинах:
⚠️ Для того, чтобы понимать остальную часть статьи без каких-либо проблем, необходимо прочитать и понять часть 1 серии.
Отмена вызова
При запуске нескольких корутин, может возникнуть сложность из-за их отслеживания или отмены каждой по отдельности. Скорее всего, мы можем рассчитывать на отмену всего объема запускаемых корутин, поскольку это приведет к отмене всех созданных дочерних экземпляров:
// предположим, что у нас есть область, определенная для этого слоя приложения
val job1 = scope.launch < … >
val job2 = scope.launch < … >
scope.cancel()
Отмена сферы действия корутины отменяет ее дочерние элементы
Иногда вам может потребоваться отменить только одну корутину, возможно, в качестве реакции на ввод данных пользователем. Вызов job1.cancel гарантирует, что только эта конкретная корутина будет отменена, а все остальные родственные элементы не будут затронуты:
// предположим, что у нас есть область, определенная для этого слоя приложения
val job1 = scope.launch < … >
val job2 = scope.launch < … >
// Первая корутина будет отменена, а вторая нет
job1.cancel()
Отмененный дочерний элемент не влияет на своих родственников
Корутины обрабатывают отмену, создавая специальное исключение: CancellationException . Если вы хотите предоставить более подробную информацию о причине отмены, возможно предоставить экземпляр CancellationException при вызове .cancel поскольку это — полная сигнатура метода:
fun cancel(cause: CancellationException? = null)
Если вы не предоставите свой собственный экземпляр CancellationException , то будет создано исключение CancellationException по умолчанию (полный код здесь):
public override fun cancel(cause: CancellationException?) cancelInternal(cause ?: defaultCancellationException())
>
Поскольку возникает исключение CancellationException , то вы сможете использовать этот механизм для обработки отмены корутины. Подробнее о том, как это сделать, читайте в разделе Обработка побочных эффектов отмены ниже.
Дочернее задание незаметно уведомляет своего родителя об отмене через исключение. Родитель использует причину отмены, чтобы определить, нужно ли ему обрабатывать исключение. Если наследник был отменен из-за CancellationException , то для родителя не требуется никаких других действий.
⚠️ После отмены области, вы больше не сможете запускать в ней новые корутины.
При использовании библиотеки androidx KTX, в большинстве случаев вы не создаете свои собственные области видимости и поэтому не несете ответственности за их отмену. Если вы работаете в области ViewModel , используя viewModelScope или, если хотите запустить корутины, привязанные к области жизненного цикла, можно использовать lifecycleScope . И viewModelScope , и lifecycleScope — это объекты CoroutineScope , которые отменяются в нужное время. Например, когда ViewModel очищается, он отменяет корутины, запущенные в его области видимости.
Почему моя работа корутины не прекращается?
Если мы просто вызовем cancel , это не будет означать, что работа корутины просто остановится. Если вы выполняете какое-то относительно тяжелое вычисление, например, чтение из нескольких файлов, то нет ничего, что бы автоматически остановило выполнение вашего кода.
Давайте возьмем более простой пример и посмотрим, что произойдет. Предположим, что нам нужно печатать “Hello” дважды в секунду, используя корутины. Мы собираемся дать ей поработать секунду, а затем отменить ее. Одна из версий реализации может выглядеть так:
Давайте посмотрим, что происходит шаг за шагом. При вызове launch , мы создаем новую сопрограмму в активном состоянии. Мы даем корутине работать за 1000 мс. Итак, теперь мы видим напечатанные:
Hello 0
Hello 1
Hello 2
После вызова job.cancel наша корутина переходит в состояние Отмены. Но затем мы видим, что Hello 3 и Hello 4 печатаются на терминале. Только после того, как работа выполнена, корутина переходит в Отмененное состояние.
Работа корутины не просто останавливается, когда вызывается отмена. Скорее всего, нам нужно изменить наш код и проверить, активна ли корутина во времени.
Отмена кода корутины должна быть совместной!
Что делает вашу работу корутины отменяемой
Вы должны убедиться, что вся работа корутины, которую реализуете, совместима с отменой, поэтому вам нужно периодически проверять отмену или перед началом любой длительной работы. Например, если вы читаете несколько файлов с диска, то прежде чем начать чтение каждого файла, проверьте, была ли отменена корутина или нет. Таким образом, процессор не выполняет лишнюю работу, когда она больше не нужна.
val job = launch for(file in files) // TODO проверка отмены
readFile(file)
>
>
Все функции приостановки работы от kotlinx.coroutines могут быть отменены: withContext , delay и т. д. Поэтому, если вы используете любой из них, не нужно проверять отмену и останавливать выполнение или создавать исключение CancellationException . Но, если вы их не используете, то, чтобы сделать ваш код корутины совместимым, есть два варианта:
- Проверка job.isActive или ensureActive()
- Позволить другой работе проходить через yield()
Проверка активного состояния задания
// Поскольку мы находимся в стартовом блоке, у нас есть доступ к job.isActive
while (i < 5 && isActive)
Это означает, что наша работа должна выполняться только тогда, когда корутина активна. Это также означает, что после того, как мы выйдем из while и захотим выполнить какое-то другое действие, например логгирование, то если задание было отменено, мы можем добавить проверку для !isActive и сделать наши действия там.
Библиотека Coroutines предоставляет еще один полезный метод — ensureActive() . Его реализация заключается в следующем:
fun Job.ensureActive(): Unit if (!isActive) throw getCancellationException()
>
>
Поскольку этот метод мгновенно выбрасывает исключение, если задание неактивно, мы можем сделать это первым делом в нашем цикле while:
while (i < 5) ensureActive()
…
>
Используя ensureActive , вы самостоятельно избегаете реализации оператора if, требуемого isActive , уменьшая объем шаблонного кода, который нужно написать, но теряете гибкость для выполнения любых других действий, таких как ведение журнала.
Позвольте другой работе протекать используя yield()
Если работа, которую вы выполняете, является 1) тяжелой для процессора, 2) может исчерпать пул потоков и 3) вы хотите позволить потоку выполнять другую работу без необходимости добавлять дополнительные потоки в пул, то используйте yield() . Первая операция, выполняемая yield, будет проверкой завершения и выходом из корутины, вызвав исключение CancellationException , если задание уже завершено. yield может быть первой функцией, вызываемой в периодической проверке, например ensureActive() , упомянутой выше.
Job.join vs отмена Deferred.await
Есть два способа дождаться результата от корутины: задания, возвращенные из launch , могут вызывать join , а Deferred (тип Job ), возвращенные из async , могут быть обработаны await .
job.join , приостанавливает работу корутины до тех пор, пока работа не будет завершена. Вместе с job.cancel она ведет себя, как вы и ожидали:
• Если вы вызываете job.cancel , а потом job.join , корутина будет приостановлена до тех пор, пока работа не будет завершена.
• Вызов job.cancel после выполнения job.join не имеет эффекта, так как задание уже выполнено.
Следует использовать Deferred , когда вас интересует результат выполнения корутины. Этот результат возвращается Deferred.await , когда корутина будет завершена. Deferred — это тип Job , и эта функция также может быть отменена.
Вызов await на отложенном, который был отменен, вызывает исключение JobCancellationException .
val deferred = async < … >deferred.cancel()
val result = deferred.await() // вызов JobCancellationException!
Вот почему мы получаем исключение: роль await состоит в том, чтобы приостановить корутину до тех пор, пока результат не будет вычислен; но поскольку она отменена, он не может быть вычислен. Поэтому вызов функции await после отмены приводит к исключению JobCancellationException: Job was cancelled .
С другой стороны, если вы вызываете deferred.cancel после deferred.await , то ничего не произойдет, так как корутина уже завершена.
Обработка побочных эффектов отмены
Предположим, что вы хотите выполнить определенное действие при отмене корутины: закрыть все ресурсы, которые могли бы использовать, протоколировать отмену или какой-то другой код очистки, который хотите выполнить. Есть несколько способов сделать это:
Проверьте ! isactive
Если вы периодически проверяете наличие isActive , то после выхода из цикла while можете очистить ресурсы. Наш код выше может быть обновлен до:
while (i < 5 && isActive) // выводит сообщение дважды в секунду
if (…) println(“Hello $”)
nextPrintTime += 500L
>
>
// работа корутины завершена, поэтому мы можем совершить очистку
println(“Clean up!”)
Посмотрите на это в действии здесь.
Так что теперь, когда корутина больше не активна, произойдет выход из while , и мы сможем сделать нашу очистку.
Наконец про try catch
Поскольку исключение CancellationException возникает при отмене корутины, то мы можем обернуть нашу приостановленную работу в try/catch и в блоке finally реализовать нашу работу по очистке.
val job = launch try work()
> catch (e: CancellationException) println(“Work cancelled!”)
> finally println(“Clean up!”)
>
>delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!”)
Но если работа по очистке, которую нам нужно выполнить, приостанавливается, приведенный выше код больше не будет работать, так как после того, как корутина находится в состоянии Отмены, она больше не может приостановиться. Смотрите полный код здесь.
Корутина в состоянии отмены не может быть приостановлена!
Чтобы иметь возможность вызывать функции suspend при отмене корутины, нам нужно будет переключить работу очистки, которую мы должны выполнить в NonCancellableCoroutineContext . Это позволит коду приостановить работу и будет удерживать корутину в состоянии Отмены до тех пор, пока работа не будет завершена.
Посмотрите, как это работает на практике здесь.
suspendCancellableCoroutine и invokeOnCancellation
Если вы преобразовали обратные вызовы в корутины с помощью метода suspendCoroutine , то вместо этого лучше использовать suspendCancellableCoroutine . Предстоящая работа по отмене может быть осуществлена с помощью продолжения continuation.invokeOnCancellation :
suspend fun work() return suspendCancellableCoroutine < continuation ->
continuation.invokeOnCancellation <
// do cleanup
>
// оставшаяся часть реализации
>
Чтобы понять преимущества структурированного параллелизма и убедиться, что мы не выполняем ненужную работу, необходимо убедиться, что вы делаете свой код отменяемым.
Используйте CoroutinesScopes , определенные в Jetpack: viewModelScope или lifecycleScope , которые отменяют свою работу, когда их область завершается. Если вы создаете свой собственный CoroutineScope , убедитесь, что привязываете его к заданию и вызываете отмену, когда это необходимо.
Отмена кода корутины должна быть совместной, поэтому убедитесь, что обновили свой код для проверки отмены, чтобы сэкономить силы и не выполнять больше работы, чем необходимо.
- Как и для чего использовать нативную библиотеку сериализации Kotlin
- Kotlin. Коллекции и последовательности
- Java против Kotlin. Android
Как правильно остановить корутину
Корутина работает благодаря тому, что event loop раз за разом вызывает её метод coroutine.send(…) . Если перестать вызывать send , то и корутина перестанет работать. Это простой и изящный способ остановить корутину, но не все так просто.
Рассмотрим пример. У нас есть компьютерная игра, которая хранит в базе данных достижения игрока. Функция работает всю игру, и каждые 5 секунд сохраняет в БД список достижений:
async def save_achivements(user_id): connection = open_connection() while True: achivements = get_achivements(user_id) await connection.send(key=user_id, value=achivements) await asyncio.sleep(5) connection.close()
С этим кодом есть проблема. Если случится ошибка внутри while True: , то до закрытия соединения с БД дело так и не дойдет. Возникнет исключение, нормальный поток исполнения команд прервется и connection.close() никто не вызовет. От такой проблемы спасает try finally :
async def save_achivements(user_id): connection = open_connection() try: while True: achivements = get_achivements(user_id) await connection.send(key=user_id, value=achivements) await asyncio.sleep(5) finally: connection.close()
Теперь в случае ошибки сработает код внутри finally и соединение будет закрыто вызовом connection.close() , ни смотря ни на что. Это победа!
А что произойдет, если корутину save_achivements(…) просто перестанут вызывать? Игрок прервал игру, корутина save_achivements(…) перестала быть нужной, поэтому её выкинули из event loop и соединение осталось незакрытым. Простой и элегантный способ остановки корутины сломал нам try finally .
Из-за этой проблемы у корутин есть еще один метод — coroutine.throw(…) . Он принимает объект исключения — coroutine.throw(exc_obj) — и забрасывает его внутрь корутины. Исключение всплывает по стеку вызовов корутин, как в обычных синхронных функциях.
CancelledError
Event loop, встроенный в библиотеку asyncio никогда не прерывает работу корутин, вместо этого он просит их остановиться самим. Его просьба — это исключение CancelledError . Его можно перехватить и обработать, как обычное исключение. Воспользуемся этим фактом, чтобы перед выходом из игры записать в БД дату последнего сохранения:
import time import asyncio async def save_achivements(user_id): connection = open_connection() try: while True: achivements = get_achivements(user_id) await connection.send(key=user_id, value=achivements) await asyncio.sleep(5) except asyncio.CancelledError: timestamp = time.time() await connection.send(key=user_id, value=achivements) await connection.send(key='last_update', value=timestamp) # отпускаем перехваченный CancelledError raise finally: connection.close()
Внутри asyncio even loop происходит примерно следующее:
coroutine = save_achivements(user_id) coroutine.send(...) coroutine.send(...) ... coroutine.throw(asyncio.CancelledError()) coroutine.send(...) coroutine.send(...) ...
Обратите внимание, что CancelledError не заблокировал работу event loop. Метод coroutine.send(…) продолжает вызываться, авейты исправно работают, а значит, завершение работы корутины тоже может быть асинхронным с вызовами await connection.send(…) .
Никто не может остановить корутину, пока та сама не пожелает. Внешний код будет терпеливо ждать, когда же корутина закончит работу и, чтобы он узнал об этом, в конце отпускаем перехваченный CancelledError . Event loop asyncio поймает это исключение и уведомит всех, кто ждал этого завершения. Подробнее читайте в документации по asyncio.Task.
Остановить корутину может только сама корутина
StopIteration vs CancelledError
StopIteration случается, когда корутина уже дошла до return и успела завершить свою работу. Это исключение нельзя перехватить внутри корутины, оно существует только снаружи.
CancelledError — это исключение просит корутину об остановке. Его специально пробрасывают внутрь, и его можно перехватить. От появления CancelledError до фактического завершения работы корутины может пройти много времени, это остается на её усмотрение.
CancelledError просит об остановке, а StopIteration — её констатирует
Попробуйте бесплатные уроки по Python
Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.
Переходите на страницу учебных модулей «Девмана» и выбирайте тему.