Skip to content

Added docs on withTimeout asynchrony and its use with resources #2252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions coroutines-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m
* <a name='closing-resources-with-finally'></a>[Closing resources with `finally`](docs/cancellation-and-timeouts.md#closing-resources-with-finally)
* <a name='run-non-cancellable-block'></a>[Run non-cancellable block](docs/cancellation-and-timeouts.md#run-non-cancellable-block)
* <a name='timeout'></a>[Timeout](docs/cancellation-and-timeouts.md#timeout)
* <a name='asynchronous-timeout-and-resources'></a>[Asynchronous timeout and resources](docs/cancellation-and-timeouts.md#asynchronous-timeout-and-resources)
<!--- TOC_REF docs/composing-suspending-functions.md -->
* <a name='composing-suspending-functions'></a>[Composing Suspending Functions](docs/composing-suspending-functions.md#composing-suspending-functions)
* <a name='sequential-by-default'></a>[Sequential by default](docs/composing-suspending-functions.md#sequential-by-default)
Expand Down
109 changes: 109 additions & 0 deletions docs/cancellation-and-timeouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<!--- END -->

Expand Down Expand Up @@ -355,6 +356,114 @@ 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 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.

<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">

```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
```

</div>

> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt).

<!--- 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 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.

<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">

```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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example could be simplified with withTimeoutOrNull to avoid try-finally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-finally here is used here because we can crash when "doing something else" in this code and we still need to close the resource after that.

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
}
```

</div>

> 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.

<!--- TEST
0
-->

<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
Expand Down
36 changes: 32 additions & 4 deletions kotlinx-coroutines-core/common/src/Timeout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -48,7 +55,14 @@ public suspend fun <T> 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 <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
Expand All @@ -68,7 +82,14 @@ public suspend fun <T> 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.
*/
Expand Down Expand Up @@ -101,7 +122,14 @@ public suspend fun <T> 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 <T> withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
Expand Down
31 changes: 31 additions & 0 deletions kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt
Original file line number Diff line number Diff line change
@@ -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
}
36 changes: 36 additions & 0 deletions kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,11 @@ class CancellationGuideTest {
"Result is null"
)
}

@Test
fun testExampleCancel09() {
test("ExampleCancel09") { kotlinx.coroutines.guide.exampleCancel09.main() }.verifyLines(
"0"
)
}
}