From 96a999f86c7cf56c392bd33796921beb258d07aa Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 10 Oct 2024 14:06:44 +0200 Subject: [PATCH] Improve the yield() documentation --- kotlinx-coroutines-core/common/src/Yield.kt | 138 ++++++++++++++++++-- 1 file changed, 129 insertions(+), 9 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index 0598228640..bbb85a102b 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -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) { + * 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() + * 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