-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve the yield() documentation #4260
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,23 +4,143 @@ import kotlinx.coroutines.internal.* | |
import kotlin.coroutines.intrinsics.* | ||
|
||
/** | ||
* Yields the thread (or thread pool) of the current coroutine dispatcher | ||
* to other coroutines on the same dispatcher to run if possible. | ||
* Suspends this coroutine and immediately schedules it for further execution. | ||
* | ||
* A coroutine run uninterrupted on a thread until the coroutine *suspend*, | ||
* giving other coroutines a chance to use that thread for their own computations. | ||
* Normally, coroutines suspend whenever they wait for something to happen: | ||
* for example, trying to receive a value from a channel that's currently empty will suspend. | ||
* Sometimes, a coroutine does not need to wait for anything, | ||
* but we still want it to give other coroutines a chance to run. | ||
* Calling [yield] has this effect: | ||
* | ||
* ``` | ||
* fun updateProgressBar(value: Int, marker: String) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the |
||
* print(marker) | ||
* } | ||
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1) | ||
* withContext(singleThreadedDispatcher) { | ||
* launch { | ||
* repeat(5) { | ||
* updateProgressBar(it, "A") | ||
* yield() | ||
* } | ||
* } | ||
* launch { | ||
* repeat(5) { | ||
* updateProgressBar(it, "B") | ||
* yield() | ||
* } | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* In this example, without the [yield], first, `A` would run its five stages of work to completion, and only then | ||
* would `B` even start executing. | ||
* With both `yield` calls, the coroutines share the single thread with each other after each stage of work. | ||
* This is useful when several coroutines running on the same thread (or thread pool) must regularly publish | ||
* their results for the program to stay responsive. | ||
* | ||
* This suspending function is cancellable: if the [Job] of the current coroutine is cancelled while | ||
* [yield] is invoked or while waiting for dispatch, it immediately resumes with [CancellationException]. | ||
* There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled | ||
* while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details. | ||
* | ||
* **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend. | ||
* **Note**: if there is only a single coroutine executing on the current dispatcher, | ||
* it is possible that [yield] will not actually suspend. | ||
* However, even in that case, the [check for cancellation][ensureActive] still happens. | ||
* | ||
* **Note**: if there is no [CoroutineDispatcher] in the context, it does not suspend. | ||
* | ||
* ## Pitfall: using `yield` to wait for something to happen | ||
* | ||
* Using `yield` for anything except a way to ensure responsiveness is often a problem. | ||
* When possible, it is recommended to structure the code in terms of coroutines waiting for some events instead of | ||
* yielding. | ||
* Below, we list the common problems involving [yield] and outline how to avoid them. | ||
* | ||
* ### Case 1: using `yield` to ensure a specific interleaving of actions | ||
* | ||
* ``` | ||
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1) | ||
* withContext(singleThreadedDispatcher) { | ||
* var value: Int? = null | ||
* val job = launch { // a new coroutine on the same dispatcher | ||
* // yield() // uncomment to see the crash | ||
* value = 42 | ||
* println("2. Value provided") | ||
* } | ||
* check(value == null) | ||
* println("No value yet!") | ||
* println("1. Awaiting the value...") | ||
* // ANTIPATTERN! DO NOT WRITE SUCH CODE! | ||
* yield() // allow the other coroutine to run | ||
* // job.join() // would work more reliably in this scenario! | ||
* check(value != null) | ||
* println("3. Obtained $value") | ||
* } | ||
* ``` | ||
* | ||
* Here, [yield] allows `singleThreadedDispatcher` to execute the task that ultimately provides the `value`. | ||
* Without the [yield], the `value != null` check would be executed directly after `Awaiting the value` is printed. | ||
* However, if the value-producing coroutine is modified to suspend before providing the value, this will | ||
* no longer work; explicitly waiting for the coroutine to finish via [Job.join] instead is robust against such changes. | ||
* | ||
* Therefore, it is an antipattern to use `yield` to synchronize code across several coroutines. | ||
* | ||
* ### Case 2: using `yield` in a loop to wait for something to happen | ||
* | ||
* ``` | ||
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1) | ||
* withContext(singleThreadedDispatcher) { | ||
* var value: Int? = null | ||
* val job = launch { // a new coroutine on the same dispatcher | ||
* delay(1.seconds) | ||
* value = 42 | ||
* } | ||
* // ANTIPATTERN! DO NOT WRITE SUCH CODE! | ||
* while (value == null) { | ||
* yield() // allow the other coroutines to run | ||
* } | ||
* println("Obtained $value") | ||
* } | ||
* ``` | ||
* | ||
* This example will lead to correct results no matter how much the value-producing coroutine suspends, | ||
* but it is still flawed. | ||
* For the one second that it takes for the other coroutine to obtain the value, | ||
* `value == null` would be constantly re-checked, leading to unjustified resource consumption. | ||
* | ||
* In this specific case, [CompletableDeferred] can be used instead: | ||
* | ||
* ``` | ||
* val singleThreadedDispatcher = Dispatchers.Default.limitedParallelism(1) | ||
* withContext(singleThreadedDispatcher) { | ||
* val deferred = CompletableDeferred<Int>() | ||
* val job = launch { // a new coroutine on the same dispatcher | ||
* delay(1.seconds) | ||
* deferred.complete(42) | ||
* } | ||
* val value = deferred.await() | ||
* println("Obtained $value") | ||
* } | ||
* ``` | ||
* | ||
* `while (channel.isEmpty) { yield() }; channel.receive()` can be replaced with just `channel.receive()`; | ||
* `while (job.isActive) { yield() }` can be replaced with [`job.join()`][Job.join]; | ||
* in both cases, this will avoid the unnecessary work of checking the loop conditions. | ||
* In general, seek ways to allow a coroutine to stay suspended until it actually has useful work to do. | ||
* | ||
* ## Implementation details | ||
* | ||
* ### Implementation details | ||
* Some coroutine dispatchers include optimizations that make yielding different from normal suspensions. | ||
* For example, when yielding, [Dispatchers.Unconfined] checks whether there are any other coroutines in the event | ||
* loop where the current coroutine executes; if not, the sole coroutine continues to execute without suspending. | ||
* Also, `Dispatchers.IO` and `Dispatchers.Default` on the JVM tweak the scheduling behavior to improve liveness | ||
* when `yield()` is used in a loop. | ||
* | ||
* If the coroutine dispatcher is [Unconfined][Dispatchers.Unconfined], this | ||
* functions suspends only when there are other unconfined coroutines working and forming an event-loop. | ||
* For other dispatchers, this function calls [CoroutineDispatcher.dispatch] and | ||
* always suspends to be resumed later regardless of the result of [CoroutineDispatcher.isDispatchNeeded]. | ||
* If there is no [CoroutineDispatcher] in the context, it does not suspend. | ||
* For custom implementations of [CoroutineDispatcher], this function checks [CoroutineDispatcher.isDispatchNeeded] and | ||
* then invokes [CoroutineDispatcher.dispatch] regardless of the result; no way is provided to change this behavior. | ||
*/ | ||
public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont -> | ||
val context = uCont.context | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.