type | layout | title | url |
---|---|---|---|
doc |
reference |
Отмена корутин и тайм-ауты |
В этом разделе рассматривается отмена корутин и тайм-ауты.
В долго работающем приложении вам может понадобиться детальное управление фоновыми корутинами. Например, пользователь
может закрыть страницу, которая запускала корутину, из-за чего её результат больше не нужен, и её действие можно
отменить. Функция 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.")
Полный код находится здесь.
Этот код выведет следующее:
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 ${i++} ...")
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.")
Полный код находится здесь.
Запустив этот код, вы увидите как корутина продолжает выводить на экран "I'm sleeping" даже после отмены, пока job
не
завершится после пяти итераций.
Есть два способа сделать вычислительный код отменяемым. Первый – периодически вызвать suspend-функцию, которая
проверяет, активна ли корутина. Для этого хорошо подходит функция
yield
. Другой —
явно проверять статус отмены. Попробуем этот подход.
Замените while (i < 5)
в предыдущем примере на while (isActive)
и запустите его ещё раз.
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 ${i++} ...")
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.")
Полный код находится здесь
Как вы можете увидеть, теперь цикл отменяется.
isActive
—
это extension-параметр, доступный внутри корутины, благодаря объекту
CoroutineScope
.
Отменяемые 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.")
Полный код находится здесь.
И 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.")
Полный код находится здесь.
Самая очевидная практическая причина отменить выполнение корутины - время её выполнения превысило некоторый тайм-аут.
Хотя можно вручную отслеживать обращение к соответствующему Job
и запускать отдельную корутину для отмены отслеживаемой после тайм-аута, есть готовая к использованию функция
withTimeout
,
которая делает это. Посмотрите на следующий пример:
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
Полный код находится здесь.
Этот код выведет следующее:
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")
Полный код находится здесь.
Теперь ошибки при выполнении этой корутины не будет:
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
}
Полный код находится здесь.
Если вы запустите приведенный выше код, вы увидите, что он не всегда выводит ноль, хотя это может зависеть от таймингов вашей машины, вам может потребоваться настроить тайм-ауты в этом примере, чтобы действительно увидеть ненулевые значения.
Обратите внимание, что увеличение и уменьшение счетчика
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
Полный код находится здесь.
Этот пример всегда выводит 0, а ресурсы не утекают.