| <!--- TEST_NAME CancellationGuideTest --> |
| |
| [//]: # (title: Cancellation and timeouts) |
| |
| This section covers coroutine cancellation and timeouts. |
| |
| ## Cancelling coroutine execution |
| |
| In a long-running application you might need fine-grained control on your background coroutines. |
| For example, a user might have closed the page that launched a coroutine and now its result |
| is no longer needed and its operation can be cancelled. |
| The [launch] function returns a [Job] that can be used to cancel the running coroutine: |
| |
| ```kotlin |
| 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-01.kt). |
| > |
| {type="note"} |
| |
| It produces the following output: |
| |
| ```text |
| 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. |
| ``` |
| |
| <!--- TEST --> |
| |
| As soon as main invokes `job.cancel`, we don't see any output from the other coroutine because it was cancelled. |
| There is also a [Job] extension function [cancelAndJoin] |
| that combines [cancel][Job.cancel] and [join][Job.join] invocations. |
| |
| ## Cancellation is cooperative |
| |
| Coroutine cancellation is _cooperative_. A coroutine code has to cooperate to be cancellable. |
| All the suspending functions in `kotlinx.coroutines` are _cancellable_. They check for cancellation of |
| coroutine and throw [CancellationException] when cancelled. However, if a coroutine is working in |
| a computation and does not check for cancellation, then it cannot be cancelled, like the following |
| example shows: |
| |
| ```kotlin |
| 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-02.kt). |
| > |
| {type="note"} |
| |
| Run it to see that it continues to print "I'm sleeping" even after cancellation |
| until the job completes by itself after five iterations. |
| |
| <!--- TEST |
| job: I'm sleeping 0 ... |
| job: I'm sleeping 1 ... |
| job: I'm sleeping 2 ... |
| main: I'm tired of waiting! |
| job: I'm sleeping 3 ... |
| job: I'm sleeping 4 ... |
| main: Now I can quit. |
| --> |
| |
| The same problem can be observed by catching a [CancellationException] and not rethrowing it: |
| |
| ```kotlin |
| import kotlinx.coroutines.* |
| |
| fun main() = runBlocking { |
| //sampleStart |
| val job = launch(Dispatchers.Default) { |
| repeat(5) { i -> |
| try { |
| // print a message twice a second |
| println("job: I'm sleeping $i ...") |
| delay(500) |
| } catch (e: Exception) { |
| // log the exception |
| println(e) |
| } |
| } |
| } |
| 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt). |
| > |
| {type="note"} |
| |
| While catching `Exception` is an anti-pattern, this issue may surface in more subtle ways, like when using the |
| [`runCatching`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run-catching.html) function, |
| which does not rethrow [CancellationException]. |
| |
| ## Making computation code cancellable |
| |
| There are two approaches to making computation code cancellable. The first one is to periodically |
| invoke a suspending function that checks for cancellation. There is a [yield] function that is a good choice for that purpose. |
| The other one is to explicitly check the cancellation status. Let us try the latter approach. |
| |
| Replace `while (i < 5)` in the previous example with `while (isActive)` and rerun it. |
| |
| ```kotlin |
| 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt). |
| > |
| {type="note"} |
| |
| As you can see, now this loop is cancelled. [isActive] is an extension property |
| available inside the coroutine via the [CoroutineScope] object. |
| |
| <!--- TEST |
| 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. |
| --> |
| |
| ## Closing resources with `finally` |
| |
| Cancellable suspending functions throw [CancellationException] on cancellation, which can be handled in |
| the usual way. For example, the `try {...} finally {...}` expression and Kotlin's `use` function execute their |
| finalization actions normally when a coroutine is cancelled: |
| |
| ```kotlin |
| 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt). |
| > |
| {type="note"} |
| |
| Both [join][Job.join] and [cancelAndJoin] wait for all finalization actions to complete, |
| so the example above produces the following output: |
| |
| ```text |
| 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. |
| ``` |
| |
| <!--- TEST --> |
| |
| ## Run non-cancellable block |
| |
| Any attempt to use a suspending function in the `finally` block of the previous example causes |
| [CancellationException], because the coroutine running this code is cancelled. Usually, this is not a |
| problem, since all well-behaving closing operations (closing a file, cancelling a job, or closing any kind of a |
| communication channel) are usually non-blocking and do not involve any suspending functions. However, in the |
| rare case when you need to suspend in a cancelled coroutine you can wrap the corresponding code in |
| `withContext(NonCancellable) {...}` using [withContext] function and [NonCancellable] context as the following example shows: |
| |
| ```kotlin |
| 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt). |
| > |
| {type="note"} |
| |
| <!--- TEST |
| 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 |
| job: And I've just delayed for 1 sec because I'm non-cancellable |
| main: Now I can quit. |
| --> |
| |
| ## Timeout |
| |
| The most obvious practical reason to cancel execution of a coroutine |
| is because its execution time has exceeded some timeout. |
| While you can manually track the reference to the corresponding [Job] and launch a separate coroutine to cancel |
| the tracked one after delay, there is a ready to use [withTimeout] function that does it. |
| Look at the following example: |
| |
| ```kotlin |
| import kotlinx.coroutines.* |
| |
| fun main() = runBlocking { |
| //sampleStart |
| withTimeout(1300L) { |
| repeat(1000) { i -> |
| println("I'm sleeping $i ...") |
| delay(500L) |
| } |
| } |
| //sampleEnd |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt). |
| > |
| {type="note"} |
| |
| It produces the following output: |
| |
| ```text |
| 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 |
| ``` |
| |
| <!--- TEST STARTS_WITH --> |
| |
| The `TimeoutCancellationException` that is thrown by [withTimeout] is a subclass of [CancellationException]. |
| We have not seen its stack trace printed on the console before. That is because |
| inside a cancelled coroutine `CancellationException` is considered to be a normal reason for coroutine completion. |
| However, in this example we have used `withTimeout` right inside the `main` function. |
| |
| Since cancellation is just an exception, all resources are closed in the usual way. |
| You can wrap the code with timeout in a `try {...} catch (e: TimeoutCancellationException) {...}` block if |
| you need to do some additional action specifically on any kind of timeout or use the [withTimeoutOrNull] function |
| that is similar to [withTimeout] but returns `null` on timeout instead of throwing an exception: |
| |
| ```kotlin |
| 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). |
| > |
| {type="note"} |
| |
| There is no longer an exception when running this code: |
| |
| ```text |
| I'm sleeping 0 ... |
| I'm sleeping 1 ... |
| I'm sleeping 2 ... |
| Result is null |
| ``` |
| |
| <!--- TEST --> |
| |
| ## Asynchronous timeout and resources |
| |
| <!-- |
| NOTE: Don't change this section name. It is being referenced to from within KDoc of withTimeout functions. |
| --> |
| |
| The timeout event in [withTimeout] is asynchronous with respect to the code running in its block and may happen at any time, |
| even right before the return from inside of the timeout block. Keep this in mind if you open or acquire some |
| resource inside the block that needs closing or release outside of the block. |
| |
| For example, here we imitate a closeable resource with the `Resource` class that simply keeps track of how many times |
| it was created by incrementing the `acquired` counter and decrementing the counter in its `close` function. |
| Now let us create a lot of coroutines, each of which creates a `Resource` at the end of the `withTimeout` block |
| and releases the resource outside the block. We add a small delay so that it is more likely that the timeout occurs |
| right when the `withTimeout` block is already finished, which will cause a resource leak. |
| |
| ```kotlin |
| import kotlinx.coroutines.* |
| |
| //sampleStart |
| var acquired = 0 |
| |
| class Resource { |
| init { acquired++ } // Acquire the resource |
| fun close() { acquired-- } // Release the resource |
| } |
| |
| fun main() { |
| runBlocking { |
| repeat(10_000) { // Launch 10K 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 |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). |
| > |
| {type="note"} |
| |
| <!--- CLEAR --> |
| |
| If you run the above code, you'll see that it does not always print zero, though it may depend on the timings |
| of your machine. You may need to tweak the timeout in this example to actually see non-zero values. |
| |
| > Note that incrementing and decrementing `acquired` counter here from 10K coroutines is completely thread-safe, |
| > since it always happens from the same thread, the one used by `runBlocking`. |
| > More on that will be explained in the chapter on coroutine context. |
| > |
| {type="note"} |
| |
| To work around this problem you can store a reference to the resource in a variable instead of returning it |
| from the `withTimeout` block. |
| |
| ```kotlin |
| import kotlinx.coroutines.* |
| |
| var acquired = 0 |
| |
| class Resource { |
| init { acquired++ } // Acquire the resource |
| fun close() { acquired-- } // Release the resource |
| } |
| |
| fun main() { |
| //sampleStart |
| runBlocking { |
| repeat(10_000) { // Launch 10K 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 |
| } |
| ``` |
| {kotlin-runnable="true" kotlin-min-compiler-version="1.3"} |
| |
| > You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-cancel-10.kt). |
| > |
| {type="note"} |
| |
| This example always prints zero. Resources do not leak. |
| |
| <!--- TEST |
| 0 |
| --> |
| |
| <!--- MODULE kotlinx-coroutines-core --> |
| <!--- INDEX kotlinx.coroutines --> |
| |
| [launch]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html |
| [Job]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html |
| [cancelAndJoin]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-and-join.html |
| [Job.cancel]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel.html |
| [Job.join]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html |
| [CancellationException]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html |
| [yield]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html |
| [isActive]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html |
| [CoroutineScope]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html |
| [withContext]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html |
| [NonCancellable]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/index.html |
| [withTimeout]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html |
| [withTimeoutOrNull]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html |
| |
| <!--- END --> |