@@ -12,10 +12,80 @@ import kotlin.jvm.JvmMultifileClass
1212import 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" )
0 commit comments