Skip to content

Commit f39e482

Browse files
authored
Improve the yield() documentation (#4260)
1 parent 13147c4 commit f39e482

File tree

1 file changed

+129
-9
lines changed
  • kotlinx-coroutines-core/common/src

1 file changed

+129
-9
lines changed

Diff for: kotlinx-coroutines-core/common/src/Yield.kt

+129-9
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,143 @@ import kotlinx.coroutines.internal.*
44
import kotlin.coroutines.intrinsics.*
55

66
/**
7-
* Yields the thread (or thread pool) of the current coroutine dispatcher
8-
* to other coroutines on the same dispatcher to run if possible.
7+
* Suspends this coroutine and immediately schedules it for further execution.
98
*
9+
* A coroutine run uninterrupted on a thread until the coroutine *suspend*,
10+
* giving other coroutines a chance to use that thread for their own computations.
11+
* Normally, coroutines suspend whenever they wait for something to happen:
12+
* for example, trying to receive a value from a channel that's currently empty will suspend.
13+
* Sometimes, a coroutine does not need to wait for anything,
14+
* but we still want it to give other coroutines a chance to run.
15+
* Calling [yield] has this effect:
16+
*
17+
* ```
18+
* fun updateProgressBar(value: Int, marker: String) {
19+
* print(marker)
20+
* }
21+
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1)
22+
* withContext(singleThreadedDispatcher) {
23+
* launch {
24+
* repeat(5) {
25+
* updateProgressBar(it, "A")
26+
* yield()
27+
* }
28+
* }
29+
* launch {
30+
* repeat(5) {
31+
* updateProgressBar(it, "B")
32+
* yield()
33+
* }
34+
* }
35+
* }
36+
* ```
37+
*
38+
* In this example, without the [yield], first, `A` would run its five stages of work to completion, and only then
39+
* would `B` even start executing.
40+
* With both `yield` calls, the coroutines share the single thread with each other after each stage of work.
41+
* This is useful when several coroutines running on the same thread (or thread pool) must regularly publish
42+
* their results for the program to stay responsive.
43+
*
1044
* This suspending function is cancellable: if the [Job] of the current coroutine is cancelled while
1145
* [yield] is invoked or while waiting for dispatch, it immediately resumes with [CancellationException].
1246
* There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled
1347
* while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details.
1448
*
15-
* **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend.
49+
* **Note**: if there is only a single coroutine executing on the current dispatcher,
50+
* it is possible that [yield] will not actually suspend.
51+
* However, even in that case, the [check for cancellation][ensureActive] still happens.
52+
*
53+
* **Note**: if there is no [CoroutineDispatcher] in the context, it does not suspend.
54+
*
55+
* ## Pitfall: using `yield` to wait for something to happen
56+
*
57+
* Using `yield` for anything except a way to ensure responsiveness is often a problem.
58+
* When possible, it is recommended to structure the code in terms of coroutines waiting for some events instead of
59+
* yielding.
60+
* Below, we list the common problems involving [yield] and outline how to avoid them.
61+
*
62+
* ### Case 1: using `yield` to ensure a specific interleaving of actions
63+
*
64+
* ```
65+
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1)
66+
* withContext(singleThreadedDispatcher) {
67+
* var value: Int? = null
68+
* val job = launch { // a new coroutine on the same dispatcher
69+
* // yield() // uncomment to see the crash
70+
* value = 42
71+
* println("2. Value provided")
72+
* }
73+
* check(value == null)
74+
* println("No value yet!")
75+
* println("1. Awaiting the value...")
76+
* // ANTIPATTERN! DO NOT WRITE SUCH CODE!
77+
* yield() // allow the other coroutine to run
78+
* // job.join() // would work more reliably in this scenario!
79+
* check(value != null)
80+
* println("3. Obtained $value")
81+
* }
82+
* ```
83+
*
84+
* Here, [yield] allows `singleThreadedDispatcher` to execute the task that ultimately provides the `value`.
85+
* Without the [yield], the `value != null` check would be executed directly after `Awaiting the value` is printed.
86+
* However, if the value-producing coroutine is modified to suspend before providing the value, this will
87+
* no longer work; explicitly waiting for the coroutine to finish via [Job.join] instead is robust against such changes.
88+
*
89+
* Therefore, it is an antipattern to use `yield` to synchronize code across several coroutines.
90+
*
91+
* ### Case 2: using `yield` in a loop to wait for something to happen
92+
*
93+
* ```
94+
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1)
95+
* withContext(singleThreadedDispatcher) {
96+
* var value: Int? = null
97+
* val job = launch { // a new coroutine on the same dispatcher
98+
* delay(1.seconds)
99+
* value = 42
100+
* }
101+
* // ANTIPATTERN! DO NOT WRITE SUCH CODE!
102+
* while (value == null) {
103+
* yield() // allow the other coroutines to run
104+
* }
105+
* println("Obtained $value")
106+
* }
107+
* ```
108+
*
109+
* This example will lead to correct results no matter how much the value-producing coroutine suspends,
110+
* but it is still flawed.
111+
* For the one second that it takes for the other coroutine to obtain the value,
112+
* `value == null` would be constantly re-checked, leading to unjustified resource consumption.
113+
*
114+
* In this specific case, [CompletableDeferred] can be used instead:
115+
*
116+
* ```
117+
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1)
118+
* withContext(singleThreadedDispatcher) {
119+
* val deferred = CompletableDeferred<Int>()
120+
* val job = launch { // a new coroutine on the same dispatcher
121+
* delay(1.seconds)
122+
* deferred.complete(42)
123+
* }
124+
* val value = deferred.await()
125+
* println("Obtained $value")
126+
* }
127+
* ```
128+
*
129+
* `while (channel.isEmpty) { yield() }; channel.receive()` can be replaced with just `channel.receive()`;
130+
* `while (job.isActive) { yield() }` can be replaced with [`job.join()`][Job.join];
131+
* in both cases, this will avoid the unnecessary work of checking the loop conditions.
132+
* In general, seek ways to allow a coroutine to stay suspended until it actually has useful work to do.
133+
*
134+
* ## Implementation details
16135
*
17-
* ### Implementation details
136+
* Some coroutine dispatchers include optimizations that make yielding different from normal suspensions.
137+
* For example, when yielding, [Dispatchers.Unconfined] checks whether there are any other coroutines in the event
138+
* loop where the current coroutine executes; if not, the sole coroutine continues to execute without suspending.
139+
* Also, `Dispatchers.IO` and `Dispatchers.Default` on the JVM tweak the scheduling behavior to improve liveness
140+
* when `yield()` is used in a loop.
18141
*
19-
* If the coroutine dispatcher is [Unconfined][Dispatchers.Unconfined], this
20-
* functions suspends only when there are other unconfined coroutines working and forming an event-loop.
21-
* For other dispatchers, this function calls [CoroutineDispatcher.dispatch] and
22-
* always suspends to be resumed later regardless of the result of [CoroutineDispatcher.isDispatchNeeded].
23-
* If there is no [CoroutineDispatcher] in the context, it does not suspend.
142+
* For custom implementations of [CoroutineDispatcher], this function checks [CoroutineDispatcher.isDispatchNeeded] and
143+
* then invokes [CoroutineDispatcher.dispatch] regardless of the result; no way is provided to change this behavior.
24144
*/
25145
public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
26146
val context = uCont.context

0 commit comments

Comments
 (0)