Skip to content

Commit ea4a81c

Browse files
committed
Document runBlocking and newCoroutineContext
1 parent e755876 commit ea4a81c

File tree

3 files changed

+156
-21
lines changed

3 files changed

+156
-21
lines changed

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@ package kotlinx.coroutines
33
import kotlin.coroutines.*
44

55
/**
6-
* Creates a context for a new coroutine. It installs [Dispatchers.Default] when no other dispatcher or
7-
* [ContinuationInterceptor] is specified and adds optional support for debugging facilities (when turned on)
8-
* and copyable-thread-local facilities on JVM.
6+
* Creates a context for a new coroutine.
7+
*
8+
* This function is used by coroutine builders to create a new coroutine context.
9+
* - It installs [Dispatchers.Default] when no other dispatcher or [ContinuationInterceptor] is specified.
10+
* - On the JVM, if the debug mode is enabled, it assigns a unique identifier to every coroutine for tracking it.
11+
* - On the JVM, copyable thread-local elements from [CoroutineScope.coroutineContext] and [context]
12+
* are copied and combined as needed.
13+
* - The elements of [context] and [CoroutineScope.coroutineContext] other than copyable thread-context ones
14+
* are combined as is, with the elements from [context] overriding the elements from [CoroutineScope.coroutineContext]
15+
* in case of equal [keys][CoroutineContext.Key].
16+
*
17+
* See the documentation of this function's JVM implementation for platform-specific details.
918
*/
1019
public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext
1120

kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,80 @@ import kotlin.jvm.JvmMultifileClass
1212
import kotlin.jvm.JvmName
1313

1414
/**
15-
* Runs a new coroutine and **blocks** the current thread until its completion.
15+
* Runs the given [block] in-place in a new coroutine based on [context],
16+
* **blocking the current thread** until its completion, and then returning its result.
1617
*
1718
* It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in
18-
* `main` functions and in tests.
19+
* `main` functions, in tests, and in non-`suspend` callbacks when `suspend` functions need to be called.
20+
*
21+
* On the JVM, if this blocked thread is interrupted (see `java.lang.Thread.interrupt`),
22+
* then the coroutine job is cancelled and this `runBlocking` invocation throws an `InterruptedException`.
23+
* On Kotlin/Native, there is no way to interrupt a thread.
24+
*
25+
* ## Structured concurrency
26+
*
27+
* The lifecycle of the new coroutine's [Job] begins with starting the [block] and completes when both the [block] and
28+
* all the coroutines launched in the scope complete.
29+
*
30+
* A new coroutine is created with the following properties:
31+
* - A new [Job] for a lexically scoped coroutine is created.
32+
* Its parent is the [Job] from the [context], if any was passed.
33+
* - If a [ContinuationInterceptor] is passed in [context],
34+
* it is used as a dispatcher of the new coroutine created by [runBlocking].
35+
* Otherwise, the new coroutine is dispatched to an event loop opened on this thread.
36+
* - The other pieces of the context are put into the new coroutine context as is.
37+
* - [newCoroutineContext] is called to optionally install debugging facilities.
38+
*
39+
* The resulting context is available in the [CoroutineScope] passed as the [block]'s receiver.
40+
*
41+
* Because the new coroutine is lexically scoped, even if a [Job] was passed in the [context],
42+
* it will not be cancelled if [runBlocking] or some child coroutine fails with an exception.
43+
* Instead, the exception will be rethrown to the caller of this function.
44+
*
45+
* If any child coroutine in this scope fails with an exception,
46+
* the scope fails, cancelling all the other children and its own [block].
47+
* If children should fail independently, consider using [supervisorScope]:
48+
* ```
49+
* runBlocking(CoroutineExceptionHandler { _, e ->
50+
* // handle the exception
51+
* }) {
52+
* supervisorScope {
53+
* // Children fail independently here
54+
* }
55+
* }
56+
* ```
57+
*
58+
* Rephrasing this in more practical terms, the specific list of structured concurrency interactions is as follows:
59+
* - The caller's [currentCoroutineContext] *is not taken into account*, its cancellation does not affect [runBlocking].
60+
* - If the new [CoroutineScope] fails with an exception
61+
* (which happens if either its [block] or any child coroutine fails with an exception),
62+
* the exception is rethrown to the caller,
63+
* without affecting the [Job] passed in the [context] (if any).
64+
* Note that this happens on any child coroutine's failure even if [block] finishes successfully.
65+
* - Cancelling the [Job] passed in the [context] (if any) cancels the new coroutine and its children.
66+
* - [runBlocking] will only finish when all the coroutines launched in it finish.
67+
* If all of them complete without failing, the [runBlocking] returns the result of the [block] to the caller.
68+
*
69+
* ## Event loop
70+
*
71+
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes
72+
* continuations in this blocked thread until the completion of this coroutine.
73+
*
74+
* This event loop is set in a thread-local variable and is accessible to nested [runBlocking] calls and
75+
* coroutine tasks forming an event loop
76+
* (such as the tasks of [Dispatchers.Unconfined] and [MainCoroutineDispatcher.immediate]).
77+
*
78+
* Nested [runBlocking] calls may execute other coroutines' tasks instead of running their own tasks.
79+
*
80+
* When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
81+
* the specified dispatcher while the current thread is blocked (and possibly running tasks from other
82+
* [runBlocking] calls on the same thread or [Dispatchers.Unconfined]).
83+
*
84+
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
85+
*
86+
* ## Pitfalls
87+
*
88+
* ### Calling from a suspend function
1989
*
2090
* Calling [runBlocking] from a suspend function is redundant.
2191
* For example, the following code is incorrect:
@@ -25,27 +95,72 @@ import kotlin.jvm.JvmName
2595
* val data = runBlocking { // <- redundant and blocks the thread, do not do that
2696
* fetchConfigurationData() // suspending function
2797
* }
98+
* // ...
2899
* ```
29100
*
30101
* Here, instead of releasing the thread on which `loadConfiguration` runs if `fetchConfigurationData` suspends, it will
31102
* block, potentially leading to thread starvation issues.
103+
* Additionally, the [currentCoroutineContext] will be ignored, and the new computation will run in the context of
104+
* the new `runBlocking` coroutine.
32105
*
33-
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
34-
* in this blocked thread until the completion of this coroutine.
35-
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
106+
* Instead, write it like this:
36107
*
37-
* When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
38-
* the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
39-
* then this invocation uses the outer event loop.
108+
* ```
109+
* suspend fun loadConfiguration() {
110+
* val data = fetchConfigurationData() // suspending function
111+
* // ...
112+
* ```
113+
*
114+
* ### Sharing tasks between [runBlocking] calls
115+
*
116+
* The event loop used by [runBlocking] is shared with the other [runBlocking] calls.
117+
* This can lead to surprising and undesired behavior.
118+
*
119+
* ```
120+
* runBlocking {
121+
* val job = launch {
122+
* delay(50.milliseconds)
123+
* println("Hello from the outer child coroutine")
124+
* }
125+
* runBlocking {
126+
* println("Entered the inner runBlocking")
127+
* delay(100.milliseconds)
128+
* println("Leaving the inner runBlocking")
129+
* }
130+
* }
131+
* ```
132+
*
133+
* This outputs the following:
134+
*
135+
* ```
136+
* Entered the inner runBlocking
137+
* Hello from the outer child coroutine
138+
* Leaving the inner runBlocking
139+
* ```
140+
*
141+
* For example, the following code may fail with a stack overflow error:
40142
*
41-
* If this blocked thread is interrupted (see `Thread.interrupt`), then the coroutine job is cancelled and
42-
* this `runBlocking` invocation throws `InterruptedException`.
143+
* ```
144+
* runBlocking {
145+
* repeat(1000) {
146+
* launch {
147+
* try {
148+
* runBlocking {
149+
* // do nothing
150+
* }
151+
* } catch (e: Throwable) {
152+
* println(e)
153+
* }
154+
* }
155+
* }
156+
* }
157+
* ```
43158
*
44-
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
45-
* for a newly created coroutine.
159+
* The reason is that each new `runBlocking` attempts to run the task of the outer `runBlocking` coroutine inline,
160+
* but those, in turn, start new `runBlocking` calls.
46161
*
47-
* @param context the context of the coroutine. The default value is an event loop on the current thread.
48-
* @param block the coroutine code.
162+
* The specific behavior of work stealing may change in the future, but is unlikely to be fully fixed,
163+
* given how widespread [runBlocking] is.
49164
*/
50165
@OptIn(ExperimentalContracts::class)
51166
@JvmName("runBlockingK")

kotlinx-coroutines-core/jvm/src/CoroutineContext.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,21 @@ import kotlin.coroutines.*
55
import kotlin.coroutines.jvm.internal.CoroutineStackFrame
66

77
/**
8-
* Creates a context for a new coroutine. It installs [Dispatchers.Default] when no other dispatcher or
9-
* [ContinuationInterceptor] is specified and adds optional support for debugging facilities (when turned on)
10-
* and copyable-thread-local facilities on JVM.
11-
* See [DEBUG_PROPERTY_NAME] for description of debugging facilities on JVM.
8+
* Creates a context for a new coroutine.
9+
*
10+
* See the documentation of this function's common-code implementation for a general overview.
11+
* Here, the JVM-specific details are described.
12+
*
13+
* - If the [debug mode][DEBUG_PROPERTY_NAME] is enabled,
14+
* [newCoroutineContext] assigns a unique identifier to every coroutine for tracking it.
15+
* The ID is visible in [toString] of the coroutine context or its job and in the thread name,
16+
* as well as in the dumps produced by the `kotlinx-coroutines-debug` module.
17+
* - [CopyableThreadContextElement] values are combined.
18+
* If both the parent and [context] have the same [CopyableThreadContextElement]
19+
* (when their [key][CoroutineContext.Key] is equal),
20+
* the values are [merged][CopyableThreadContextElement.mergeForChild].
21+
* If only the parent or only the [context] has the [CopyableThreadContextElement],
22+
* the value is [copied][CopyableThreadContextElement.copyForChild].
1223
*/
1324
@ExperimentalCoroutinesApi
1425
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {

0 commit comments

Comments
 (0)