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"
+ )
+ }
}