Spec-Zone.ru › Kotlin 1.4

Содержание

  • Отмена и таймауты
    • Отмена выполнения корутины
    • Отмена — это кооперативный процесс
    • Делаем вычислительный код отменяемым
    • Закрытие ресурсов с finally
    • Выполнение неотменяемого блока
    • Таймаут
    • Асинхронный таймаут и ресурсы

Отмена и таймауты

В этом разделе рассматриваются отмена корутин и таймауты.

Отмена выполнения корутины

В приложениях с длительным выполнением может потребоваться точный контроль над фоновыми корутинами. Например, пользователь может закрыть страницу, запустившую корутину, и теперь её результат больше не нужен, а операция может быть отменена. Функция launch возвращает Job, который можно использовать для отмены выполняемой корутины:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    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.")
//sampleEnd    
}

Полный код вы можете найти здесь.

Это приводит к следующему результату:

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, мы не видим никакого вывода от другой корутины, потому что она была отменена. Также есть функция-расширение для Job, cancelAndJoin, которая объединяет вызовы cancel и join.

Отмена — это кооперативный процесс

Отмена корутины — это кооперативный процесс. Код корутины должен кооперировать для отмены. Все приостанавливающие функции в kotlinx.coroutines являются отменяемыми. Они проверяют отмену корутины и выбрасывают CancellationException при отмене. Однако, если корутина работает с вычислениями и не проверяет отмену, то её нельзя отменить, как показано в следующем примере:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    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.")
//sampleEnd    
}

Полный код вы можете найти здесь.

Запустите его, чтобы увидеть, что он продолжает печатать «Я сплю» даже после отмены, пока задача не завершится сама через пять итераций.

Делаем вычислительный код отменяемым

Существует два подхода к тому, чтобы сделать вычислительный код отменяемым. Первый заключается в периодическом вызове приостанавливающей функции, которая проверяет отмену. Функция yield подходит для этой цели. Другой подход — явное проверка состояния отмены. Попробуем последний подход.

Замените while (i < 5) в предыдущем примере на while (isActive) и запустите его снова.

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    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.")
//sampleEnd    
}

Полный код вы можете найти здесь.

Как видите, теперь этот цикл отменяется. Свойство isActive доступно внутри корутины через объект CoroutineScope.

Закрытие ресурсов с finally

При отмене отменяемые приостанавливающие функции выбрасывают CancellationException, которые можно обрабатывать стандартным образом. Например, выражение try {...} finally {...} и функция Kotlin use обычно выполняют свои завершающие действия при отмене корутины:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    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.")
//sampleEnd    
}

Полный код вы можете найти здесь.

Оба вызова 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.

Выполнение неотменяемого блока

Любая попытка использовать приостанавливающую функцию в блоке finally предыдущего примера приводит к CancellationException, поскольку корутина, выполняющая этот код, отменяется. Обычно это не проблема, так как все корректные операции закрытия (закрытие файла, отмена задачи или закрытие любого канала связи) обычно неблокирующие и не включают приостанавливающие функции. Однако в редких случаях, когда вам необходимо приостановить выполнение в отменённой корутине, вы можете обернуть соответствующий код в withContext(NonCancellable) {...} с помощью функции withContext и контекста NonCancellable, как показано в следующем примере:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    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.")
//sampleEnd    
}

Полный код вы можете найти здесь.

Таймаут

Самая очевидная практическая причина отмены выполнения корутины — превышение времени выполнения таймаута. Хотя вы можете вручную отслеживать ссылку на соответствующий Job и запустить отдельную корутину для отмены отслеживаемой корутины после задержки, существует готовая функция withTimeout, которая выполняет эту задачу. Посмотрите на следующий пример:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
//sampleEnd
}

Полный код вы можете найти здесь.

Это приводит к следующему результату:

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 при таймауте вместо выбрасывания исключения:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    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")
//sampleEnd
}

Полный код вы можете найти здесь.

Исключения при выполнении этого кода больше нет:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

Асинхронный таймаут и ресурсы

Событие таймаута в withTimeout является асинхронным по отношению к коду, выполняемому в его блоке, и может произойти в любое время, даже непосредственно перед возвратом изнутри блока таймаута. Имейте это в виду, если вы открываете или приобретаете какой-либо ресурс внутри блока, который требует закрытия или освобождения за пределами блока.

Например, здесь мы имитируем закрываемый ресурс с классом Resource, который просто отслеживает, сколько раз он был создан, увеличивая счетчик acquired и уменьшая этот счетчик в своей функции close. Давайте запустим много сопроцессов с небольшим таймаутом, попробуем приобрести этот ресурс изнутри блока withTimeout после небольшой задержки и освободить его снаружи.

import kotlinx.coroutines.*

//sampleStart
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
}
//sampleEnd

Полный код вы можете найти здесь.

Если вы запустите приведенный выше код, вы увидите, что он не всегда выводит ноль, хотя это может зависеть от времени вашей машины. Возможно, вам потребуется настроить таймауты в этом примере, чтобы увидеть значения, отличные от нуля.

Обратите внимание, что увеличение и уменьшение счетчика acquired здесь из 100 000 сопроцессов абсолютно безопасно, так как это всегда происходит из одной основной нити. Более подробная информация об этом будет приведена в следующей главе о контексте сопроцессов.

Чтобы обойти эту проблему, вы можете сохранить ссылку на ресурс в переменной вместо возврата ее из блока withTimeout.

import kotlinx.coroutines.*

var acquired = 0

class Resource {
    init { acquired++ } // Acquire the resource
    fun close() { acquired-- } // Release the resource
}

fun main() {
//sampleStart
    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
//sampleEnd
}

Полный код вы можете найти здесь.

В этом примере всегда выводится ноль. Ресурсы не утечкаются.

© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API