Skip to content

Commit 20ca97a

Browse files
authored
Added docs on withTimeout asynchrony and its use with resources (#2252)
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
1 parent b97ebfc commit 20ca97a

File tree

6 files changed

+216
-4
lines changed

6 files changed

+216
-4
lines changed

coroutines-guide.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m
2020
* <a name='closing-resources-with-finally'></a>[Closing resources with `finally`](docs/cancellation-and-timeouts.md#closing-resources-with-finally)
2121
* <a name='run-non-cancellable-block'></a>[Run non-cancellable block](docs/cancellation-and-timeouts.md#run-non-cancellable-block)
2222
* <a name='timeout'></a>[Timeout](docs/cancellation-and-timeouts.md#timeout)
23+
* <a name='asynchronous-timeout-and-resources'></a>[Asynchronous timeout and resources](docs/cancellation-and-timeouts.md#asynchronous-timeout-and-resources)
2324
<!--- TOC_REF docs/composing-suspending-functions.md -->
2425
* <a name='composing-suspending-functions'></a>[Composing Suspending Functions](docs/composing-suspending-functions.md#composing-suspending-functions)
2526
* <a name='sequential-by-default'></a>[Sequential by default](docs/composing-suspending-functions.md#sequential-by-default)

docs/cancellation-and-timeouts.md

+109
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* [Closing resources with `finally`](#closing-resources-with-finally)
1212
* [Run non-cancellable block](#run-non-cancellable-block)
1313
* [Timeout](#timeout)
14+
* [Asynchronous timeout and resources](#asynchronous-timeout-and-resources)
1415

1516
<!--- END -->
1617

@@ -355,6 +356,114 @@ Result is null
355356

356357
<!--- TEST -->
357358

359+
### Asynchronous timeout and resources
360+
361+
<!--
362+
NOTE: Don't change this section name. It is being referenced to from within KDoc of withTimeout functions.
363+
-->
364+
365+
The timeout event in [withTimeout] is asynchronous with respect to the code running in its block and may happen at any time,
366+
even right before the return from inside of the timeout block. Keep this in mind if you open or acquire some
367+
resource inside the block that needs closing or release outside of the block.
368+
369+
For example, here we imitate a closeable resource with the `Resource` class, that simply keeps track of how many times
370+
it was created by incrementing the `acquired` counter and decrementing this counter from its `close` function.
371+
Let us run a lot of coroutines with the small timeout try acquire this resource from inside
372+
of the `withTimeout` block after a bit of delay and release it from outside.
373+
374+
<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
375+
376+
```kotlin
377+
import kotlinx.coroutines.*
378+
379+
//sampleStart
380+
var acquired = 0
381+
382+
class Resource {
383+
init { acquired++ } // Acquire the resource
384+
fun close() { acquired-- } // Release the resource
385+
}
386+
387+
fun main() {
388+
runBlocking {
389+
repeat(100_000) { // Launch 100K coroutines
390+
launch {
391+
val resource = withTimeout(60) { // Timeout of 60 ms
392+
delay(50) // Delay for 50 ms
393+
Resource() // Acquire a resource and return it from withTimeout block
394+
}
395+
resource.close() // Release the resource
396+
}
397+
}
398+
}
399+
// Outside of runBlocking all coroutines have completed
400+
println(acquired) // Print the number of resources still acquired
401+
}
402+
//sampleEnd
403+
```
404+
405+
</div>
406+
407+
> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt).
408+
409+
<!--- CLEAR -->
410+
411+
If you run the above code you'll see that it does not always print zero, though it may depend on the timings
412+
of your machine you may need to tweak timeouts in this example to actually see non-zero values.
413+
414+
> Note, that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe,
415+
> since it always happens from the same main thread. More on that will be explained in the next chapter
416+
> on coroutine context.
417+
418+
To workaround this problem you can store a reference to the resource in the variable as opposed to returning it
419+
from the `withTimeout` block.
420+
421+
<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
422+
423+
```kotlin
424+
import kotlinx.coroutines.*
425+
426+
var acquired = 0
427+
428+
class Resource {
429+
init { acquired++ } // Acquire the resource
430+
fun close() { acquired-- } // Release the resource
431+
}
432+
433+
fun main() {
434+
//sampleStart
435+
runBlocking {
436+
repeat(100_000) { // Launch 100K coroutines
437+
launch {
438+
var resource: Resource? = null // Not acquired yet
439+
try {
440+
withTimeout(60) { // Timeout of 60 ms
441+
delay(50) // Delay for 50 ms
442+
resource = Resource() // Store a resource to the variable if acquired
443+
}
444+
// We can do something else with the resource here
445+
} finally {
446+
resource?.close() // Release the resource if it was acquired
447+
}
448+
}
449+
}
450+
}
451+
// Outside of runBlocking all coroutines have completed
452+
println(acquired) // Print the number of resources still acquired
453+
//sampleEnd
454+
}
455+
```
456+
457+
</div>
458+
459+
> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt).
460+
461+
This example always prints zero. Resources do not leak.
462+
463+
<!--- TEST
464+
0
465+
-->
466+
358467
<!--- MODULE kotlinx-coroutines-core -->
359468
<!--- INDEX kotlinx.coroutines -->
360469
[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html

kotlinx-coroutines-core/common/src/Timeout.kt

+32-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ import kotlin.time.*
2424
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
2525
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
2626
*
27-
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
27+
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
28+
* even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
29+
* resource inside the [block] that needs closing or release outside of the block.
30+
* See the
31+
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
32+
* section of the coroutines guide for details.
33+
*
34+
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
2835
*
2936
* @param timeMillis timeout time in milliseconds.
3037
*/
@@ -48,7 +55,14 @@ public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineSco
4855
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
4956
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
5057
*
51-
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
58+
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
59+
* even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
60+
* resource inside the [block] that needs closing or release outside of the block.
61+
* See the
62+
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
63+
* section of the coroutines guide for details.
64+
*
65+
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
5266
*/
5367
@ExperimentalTime
5468
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
@@ -68,7 +82,14 @@ public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineSc
6882
* The sibling function that throws an exception on timeout is [withTimeout].
6983
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
7084
*
71-
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
85+
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
86+
* even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
87+
* resource inside the [block] that needs closing or release outside of the block.
88+
* See the
89+
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
90+
* section of the coroutines guide for details.
91+
*
92+
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
7293
*
7394
* @param timeMillis timeout time in milliseconds.
7495
*/
@@ -101,7 +122,14 @@ public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend Corout
101122
* The sibling function that throws an exception on timeout is [withTimeout].
102123
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
103124
*
104-
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
125+
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
126+
* even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some
127+
* resource inside the [block] that needs closing or release outside of the block.
128+
* See the
129+
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
130+
* section of the coroutines guide for details.
131+
*
132+
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
105133
*/
106134
@ExperimentalTime
107135
public suspend fun <T> withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit.
6+
package kotlinx.coroutines.guide.exampleCancel08
7+
8+
import kotlinx.coroutines.*
9+
10+
var acquired = 0
11+
12+
class Resource {
13+
init { acquired++ } // Acquire the resource
14+
fun close() { acquired-- } // Release the resource
15+
}
16+
17+
fun main() {
18+
runBlocking {
19+
repeat(100_000) { // Launch 100K coroutines
20+
launch {
21+
val resource = withTimeout(60) { // Timeout of 60 ms
22+
delay(50) // Delay for 50 ms
23+
Resource() // Acquire a resource and return it from withTimeout block
24+
}
25+
resource.close() // Release the resource
26+
}
27+
}
28+
}
29+
// Outside of runBlocking all coroutines have completed
30+
println(acquired) // Print the number of resources still acquired
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit.
6+
package kotlinx.coroutines.guide.exampleCancel09
7+
8+
import kotlinx.coroutines.*
9+
10+
var acquired = 0
11+
12+
class Resource {
13+
init { acquired++ } // Acquire the resource
14+
fun close() { acquired-- } // Release the resource
15+
}
16+
17+
fun main() {
18+
runBlocking {
19+
repeat(100_000) { // Launch 100K coroutines
20+
launch {
21+
var resource: Resource? = null // Not acquired yet
22+
try {
23+
withTimeout(60) { // Timeout of 60 ms
24+
delay(50) // Delay for 50 ms
25+
resource = Resource() // Store a resource to the variable if acquired
26+
}
27+
// We can do something else with the resource here
28+
} finally {
29+
resource?.close() // Release the resource if it was acquired
30+
}
31+
}
32+
}
33+
}
34+
// Outside of runBlocking all coroutines have completed
35+
println(acquired) // Print the number of resources still acquired
36+
}

kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt

+7
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,11 @@ class CancellationGuideTest {
8888
"Result is null"
8989
)
9090
}
91+
92+
@Test
93+
fun testExampleCancel09() {
94+
test("ExampleCancel09") { kotlinx.coroutines.guide.exampleCancel09.main() }.verifyLines(
95+
"0"
96+
)
97+
}
9198
}

0 commit comments

Comments
 (0)