From 27b6e9cf750641003753366a5ff9b9abfa481634 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 18 Sep 2020 18:49:20 +0300 Subject: [PATCH] Added docs on withTimeout asynchrony and its use with resources This is a tricky gotcha that needs additional explanation. There are two examples added, one showing the bad code and explaining why it does not work, and the other showing the correct way to write it. Fixes #2233 --- coroutines-guide.md | 1 + docs/cancellation-and-timeouts.md | 109 ++++++++++++++++++ kotlinx-coroutines-core/common/src/Timeout.kt | 36 +++++- .../jvm/test/guide/example-cancel-08.kt | 31 +++++ .../jvm/test/guide/example-cancel-09.kt | 36 ++++++ .../test/guide/test/CancellationGuideTest.kt | 7 ++ 6 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt diff --git a/coroutines-guide.md b/coroutines-guide.md index ea512ba68d..09cfb93cab 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -20,6 +20,7 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Closing resources with `finally`](docs/cancellation-and-timeouts.md#closing-resources-with-finally) * [Run non-cancellable block](docs/cancellation-and-timeouts.md#run-non-cancellable-block) * [Timeout](docs/cancellation-and-timeouts.md#timeout) + * [Asynchronous timeout and resources](docs/cancellation-and-timeouts.md#asynchronous-timeout-and-resources) * [Composing Suspending Functions](docs/composing-suspending-functions.md#composing-suspending-functions) * [Sequential by default](docs/composing-suspending-functions.md#sequential-by-default) diff --git a/docs/cancellation-and-timeouts.md b/docs/cancellation-and-timeouts.md index d8d5b7bad4..b296bde493 100644 --- a/docs/cancellation-and-timeouts.md +++ b/docs/cancellation-and-timeouts.md @@ -11,6 +11,7 @@ * [Closing resources with `finally`](#closing-resources-with-finally) * [Run non-cancellable block](#run-non-cancellable-block) * [Timeout](#timeout) + * [Asynchronous timeout and resources](#asynchronous-timeout-and-resources) @@ -355,6 +356,114 @@ Result is null +### Asynchronous timeout and resources + + + +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 this counter from its `close` function. +Let us run a lot of coroutines with the small timeout try acquire this resource from inside +of the `withTimeout` block after a bit of delay and release it from outside. + +
+ +```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(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 +``` + +
+ +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). + + + +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 timeouts in this example to actually see non-zero values. + +> 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 next chapter +> on coroutine context. + +To workaround this problem you can store a reference to the resource in the variable as opposed to 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(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 +} +``` + +
+ +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). + +This example always prints zero. Resources do not leak. + + + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index c8e4455c92..8547358b78 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -24,7 +24,14 @@ import kotlin.time.* * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the 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. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -48,7 +55,14 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the 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. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T { @@ -68,7 +82,14 @@ public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineSc * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the 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. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -101,7 +122,14 @@ public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend Corout * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the 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. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? = diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt new file mode 100644 index 0000000000..e7def132ae --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleCancel08 + +import kotlinx.coroutines.* + +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 +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt new file mode 100644 index 0000000000..95424f5108 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleCancel09 + +import kotlinx.coroutines.* + +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 { + 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 +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt index a2e91de82d..ca2910fb63 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt @@ -87,4 +87,11 @@ class CancellationGuideTest { "Result is null" ) } + + @Test + fun testExampleCancel09() { + test("ExampleCancel09") { kotlinx.coroutines.guide.exampleCancel09.main() }.verifyLines( + "0" + ) + } }