From cc34948215393a9bbcefe8fdee690495c1bdfa1b Mon Sep 17 00:00:00 2001 From: globsterg <166155014+globsterg@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:21:23 +0300 Subject: [PATCH] Rework the KDoc for `CoroutineStart` (#4147) Extend the KDoc to include usage examples, description of best practices and pitfalls, and more. --- .../common/src/CoroutineDispatcher.kt | 56 ++- .../common/src/CoroutineStart.kt | 334 +++++++++++++++--- 2 files changed, 336 insertions(+), 54 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 8a114f6ab7..37b68760a0 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -6,23 +6,56 @@ import kotlin.coroutines.* /** * Base class to be extended by all coroutine dispatcher implementations. * + * If `kotlinx-coroutines` is used, it is recommended to avoid [ContinuationInterceptor] instances that are not + * [CoroutineDispatcher] implementations, as [CoroutineDispatcher] ensures that the + * debugging facilities in the [newCoroutineContext] function work properly. + * + * ## Predefined dispatchers + * * The following standard implementations are provided by `kotlinx.coroutines` as properties on * the [Dispatchers] object: * - * - [Dispatchers.Default] — is used by all standard builders if no dispatcher or any other [ContinuationInterceptor] - * is specified in their context. It uses a common pool of shared background threads. + * - [Dispatchers.Default] is used by all standard builders if no dispatcher or any other [ContinuationInterceptor] + * is specified in their context. + * It uses a common pool of shared background threads. * This is an appropriate choice for compute-intensive coroutines that consume CPU resources. - * - [Dispatchers.IO] — uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive _blocking_ + * - `Dispatchers.IO` (available on the JVM and Native targets) + * uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive _blocking_ * operations (like file I/O and blocking socket I/O). - * - [Dispatchers.Unconfined] — starts coroutine execution in the current call-frame until the first suspension, - * whereupon the coroutine builder function returns. - * The coroutine will later resume in whatever thread used by the - * corresponding suspending function, without confining it to any specific thread or pool. + * - [Dispatchers.Main] represents the UI thread if one is available. + * - [Dispatchers.Unconfined] starts coroutine execution in the current call-frame until the first suspension, + * at which point the coroutine builder function returns. + * When the coroutine is resumed, the thread from which it is resumed will run the coroutine code until the next + * suspension, and so on. * **The `Unconfined` dispatcher should not normally be used in code**. - * - Private thread pools can be created with [newSingleThreadContext] and [newFixedThreadPoolContext]. - * - An arbitrary [Executor][java.util.concurrent.Executor] can be converted to a dispatcher with the [asCoroutineDispatcher] extension function. + * - Calling [limitedParallelism] on any dispatcher creates a view of the dispatcher that limits the parallelism + * to the given value. + * This allows creating private thread pools without spawning new threads. + * For example, `Dispatchers.IO.limitedParallelism(4)` creates a dispatcher that allows running at most + * 4 tasks in parallel, reusing the existing IO dispatcher threads. + * - When thread pools completely separate from [Dispatchers.Default] and [Dispatchers.IO] are required, + * they can be created with `newSingleThreadContext` and `newFixedThreadPoolContext` on the JVM and Native targets. + * - An arbitrary `java.util.concurrent.Executor` can be converted to a dispatcher with the + * `asCoroutineDispatcher` extension function. + * + * ## Dispatch procedure * - * This class ensures that debugging facilities in [newCoroutineContext] function work properly. + * Typically, a dispatch procedure is performed as follows: + * + * - First, [isDispatchNeeded] is invoked to determine whether the coroutine should be dispatched + * or is already in the right context. + * - If [isDispatchNeeded] returns `true`, the coroutine is dispatched using the [dispatch] method. + * It may take a while for the dispatcher to start the task, + * but the [dispatch] method itself may return immediately, before the task has even begun to execute. + * - If no dispatch is needed (which is the case for [Dispatchers.Main.immediate][MainCoroutineDispatcher.immediate] + * when already on the main thread and for [Dispatchers.Unconfined]), + * [dispatch] is typically not called, + * and the coroutine is resumed in the thread performing the dispatch procedure, + * forming an event loop to prevent stack overflows. + * See [Dispatchers.Unconfined] for a description of event loops. + * + * This behavior may be different on the very first dispatch procedure for a given coroutine, depending on the + * [CoroutineStart] parameter of the coroutine builder. */ public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { @@ -205,7 +238,7 @@ public abstract class CoroutineDispatcher : public final override fun releaseInterceptedContinuation(continuation: Continuation<*>) { /* - * Unconditional cast is safe here: we only return DispatchedContinuation from `interceptContinuation`, + * Unconditional cast is safe here: we return only DispatchedContinuation from `interceptContinuation`, * any ClassCastException can only indicate compiler bug */ val dispatched = continuation as DispatchedContinuation<*> @@ -229,4 +262,3 @@ public abstract class CoroutineDispatcher : /** @suppress for nicer debugging */ override fun toString(): String = "$classSimpleName@$hexAddress" } - diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 865be8e334..c4c4cea723 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -1,94 +1,344 @@ package kotlinx.coroutines -import kotlinx.coroutines.CoroutineStart.* import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* /** * Defines start options for coroutines builders. - * It is used in `start` parameter of [launch][CoroutineScope.launch], [async][CoroutineScope.async], and other coroutine builder functions. + * + * It is used in the `start` parameter of coroutine builder functions like + * [launch][CoroutineScope.launch] and [async][CoroutineScope.async] + * to describe when and how the coroutine should be dispatched initially. + * + * This parameter only affects how the coroutine behaves until the code of its body starts executing. + * After that, cancellability and dispatching are defined by the behavior of the invoked suspending functions. * * The summary of coroutine start options is: - * - [DEFAULT] -- immediately schedules coroutine for execution according to its context; - * - [LAZY] -- starts coroutine lazily, only when it is needed; - * - [ATOMIC] -- atomically (in a non-cancellable way) schedules coroutine for execution according to its context; - * - [UNDISPATCHED] -- immediately executes coroutine until its first suspension point _in the current thread_. + * - [DEFAULT] immediately schedules the coroutine for execution according to its context. + * - [LAZY] delays the moment of the initial dispatch until the result of the coroutine is needed. + * - [ATOMIC] prevents the coroutine from being cancelled before it starts, ensuring that its code will start + * executing in any case. + * - [UNDISPATCHED] immediately executes the coroutine until its first suspension point _in the current thread_. */ public enum class CoroutineStart { /** - * Default -- immediately schedules the coroutine for execution according to its context. + * Immediately schedules the coroutine for execution according to its context. This is usually the default option. + * + * [DEFAULT] uses the default dispatch procedure described in the [CoroutineDispatcher] documentation. * - * If the [CoroutineDispatcher] of the coroutine context returns `true` from [CoroutineDispatcher.isDispatchNeeded] - * function as most dispatchers do, then the coroutine code is dispatched for execution later, while the code that - * invoked the coroutine builder continues execution. + * If the coroutine's [Job] is cancelled before it started executing, then it will not start its + * execution at all and will be considered [cancelled][Job.isCancelled]. * - * Note that [Dispatchers.Unconfined] always returns `false` from its [CoroutineDispatcher.isDispatchNeeded] - * function, so starting a coroutine with [Dispatchers.Unconfined] by [DEFAULT] is the same as using [UNDISPATCHED]. + * Examples: + * + * ``` + * // Example of starting a new coroutine that goes through a dispatch + * runBlocking { + * println("1. About to start a new coroutine.") + * // Dispatch the job to execute later. + * // The parent coroutine's dispatcher is inherited by default. + * // In this case, it's the single thread backing `runBlocking`. + * launch { // CoroutineStart.DEFAULT is launch's default start mode + * println("3. When the thread is available, we start the coroutine") + * } + * println("2. The thread keeps doing other work after launching the coroutine") + * } + * ``` * - * If coroutine [Job] is cancelled before it even had a chance to start executing, then it will not start its - * execution at all, but will complete with an exception. + * ``` + * // Example of starting a new coroutine that doesn't go through a dispatch initially + * runBlocking { + * println("1. About to start a coroutine not needing a dispatch.") + * // Dispatch the job to execute. + * // `Dispatchers.Unconfined` is explicitly chosen. + * launch(Dispatchers.Unconfined) { // CoroutineStart.DEFAULT is the launch's default start mode + * println("2. The body will be executed immediately") + * delay(50.milliseconds) // give up the thread to the outer coroutine + * println("4. When the thread is next available, this coroutine proceeds further") + * } + * println("3. After the initial suspension, the thread does other work.") + * } + * ``` * - * Cancellability of a coroutine at suspension points depends on the particular implementation details of - * suspending functions. Use [suspendCancellableCoroutine] to implement cancellable suspending functions. + * ``` + * // Example of cancelling coroutines before they start executing. + * runBlocking { + * // dispatch the job to execute on this thread later + * launch { // CoroutineStart.DEFAULT is the launch's default start mode + * println("This code will never execute") + * } + * cancel() // cancels the current coroutine scope and its children + * launch(Dispatchers.Unconfined) { + * println("This code will never execute") + * } + * println("This code will execute.") + * } + * ``` */ DEFAULT, /** * Starts the coroutine lazily, only when it is needed. * - * See the documentation for the corresponding coroutine builders for details - * (like [launch][CoroutineScope.launch] and [async][CoroutineScope.async]). + * Starting a coroutine with [LAZY] only creates the coroutine, but does not schedule it for execution. + * When the completion of the coroutine is first awaited + * (for example, via [Job.join]) or explicitly [started][Job.start], + * the dispatch procedure described in the [CoroutineDispatcher] documentation is performed in the thread + * that did it. + * + * The details of what counts as waiting can be found in the documentation of the corresponding coroutine builders + * like [launch][CoroutineScope.launch] and [async][CoroutineScope.async]. + * + * If the coroutine's [Job] is cancelled before it started executing, then it will not start its + * execution at all and will be considered [cancelled][Job.isCancelled]. + * + * **Pitfall**: launching a coroutine with [LAZY] without awaiting or cancelling it at any point means that it will + * never be completed, leading to deadlocks and resource leaks. + * For example, the following code will deadlock, since [coroutineScope] waits for all of its child coroutines to + * complete: + * ``` + * // This code hangs! + * coroutineScope { + * launch(start = CoroutineStart.LAZY) { } + * } + * ``` + * + * The behavior of [LAZY] can be described with the following examples: * - * If coroutine [Job] is cancelled before it even had a chance to start executing, then it will not start its - * execution at all, but will complete with an exception. + * ``` + * // Example of lazily starting a new coroutine that goes through a dispatch + * runBlocking { + * println("1. About to start a new coroutine.") + * // Create a job to execute on `Dispatchers.Default` later. + * val job = launch(Dispatchers.Default, start = CoroutineStart.LAZY) { + * println("3. Only now does the coroutine start.") + * } + * delay(10.milliseconds) // try to give the coroutine some time to run + * println("2. The coroutine still has not started. Now, we join it.") + * job.join() + * } + * ``` + * + * ``` + * // Example of lazily starting a new coroutine that doesn't go through a dispatch initially + * runBlocking { + * println("1. About to lazily start a new coroutine.") + * // Create a job to execute on `Dispatchers.Unconfined` later. + * val lazyJob = launch(Dispatchers.Unconfined, start = CoroutineStart.LAZY) { + * println("3. The coroutine starts on the thread that called `join`.") + * } + * // We start the job on another thread for illustrative purposes + * launch(Dispatchers.Default) { + * println("2. We start the lazyJob.") + * job.start() // runs lazyJob's code in-place + * println("4. Only now does the `start` call return.") + * } + * } + * ``` + * + * ## Alternatives + * + * The effects of [LAZY] can usually be achieved more idiomatically without it. + * + * When a coroutine is started with [LAZY] and is stored in a property, + * it may be a better choice to use [lazy] instead: + * + * ``` + * // instead of `val page = scope.async(start = CoroutineStart.LAZY) { getPage() }`, do + * val page by lazy { scope.async { getPage() } } + * ``` + * + * This way, the child coroutine is not created at all unless it is needed. + * Note that with this, any access to this variable will start the coroutine, + * even something like `page.invokeOnCompletion { }` or `page.isActive`. + * + * If a coroutine is started with [LAZY] and then unconditionally started, + * it is more idiomatic to create the coroutine in the exact place where it is started: + * + * ``` + * // instead of `val job = scope.launch(start = CoroutineStart.LAZY) { }; job.start()`, do + * scope.launch { } + * ``` */ LAZY, /** * Atomically (i.e., in a non-cancellable way) schedules the coroutine for execution according to its context. - * This is similar to [DEFAULT], but the coroutine cannot be cancelled before it starts executing. * - * The coroutine started with [ATOMIC] is guaranteed to start execution even if its [Job] was cancelled. - * This [CoroutineStart] option can be used to ensure resources' disposal in case of cancellation. - * For example, this `producer` guarantees that the `channel` will be eventually closed, - * even if the coroutine scope is cancelled before `producer` is called: + * This is similar to [DEFAULT], but the coroutine is guaranteed to start executing even if it was cancelled. + * This only affects the behavior until the body of the coroutine starts executing; + * inside the body, cancellation will work as usual. + * + * Like [ATOMIC], [UNDISPATCHED], too, ensures that coroutines will be started in any case. + * The difference is that, instead of immediately starting them on the same thread, + * [ATOMIC] performs the full dispatch procedure just as [DEFAULT] does. + * + * Because of this, we can use [ATOMIC] in cases where we want to be certain that some code eventually runs + * and uses a specific dispatcher to do that. + * + * Example: + * ``` + * val mutex = Mutex() + * + * mutex.lock() // lock the mutex outside the coroutine + * // ... // initial portion of the work, protected by the mutex + * val job = launch(start = CoroutineStart.ATOMIC) { + * // the work must continue in a coroutine, but still under the mutex + * println("Coroutine running!") + * try { + * // this `try` block will be entered in any case because of ATOMIC + * println("Starting task...") + * delay(10.milliseconds) // throws due to cancellation + * println("Finished task.") + * } finally { + * mutex.unlock() // correctly release the mutex + * } + * } + * + * job.cancelAndJoin() // we immediately cancel the coroutine. + * mutex.withLock { + * println("The lock has been returned correctly!") + * } + * ``` + * + * Here, we used [ATOMIC] to ensure that a mutex that was acquired outside the coroutine does get released + * even if cancellation happens between `lock()` and `launch`. + * As a result, the mutex will always be released. + * + * The behavior of [ATOMIC] can be described with the following examples: + * * ``` - * fun CoroutineScope.producer(channel: SendChannel) = + * // Example of cancelling atomically started coroutines + * runBlocking { + * println("1. Atomically starting a coroutine that goes through a dispatch.") * launch(start = CoroutineStart.ATOMIC) { + * check(!isActive) // attempting to suspend later will throw + * println("4. The coroutine was cancelled (isActive = $isActive), but starts anyway.") * try { - * // produce elements - * } finally { - * channel.close() + * delay(10.milliseconds) // will throw: the coroutine is cancelled + * println("This code will never run.") + * } catch (e: CancellationException) { + * println("5. Cancellation at later points still works.") + * throw e * } * } + * println("2. Cancelling this coroutine and all of its children.") + * cancel() + * launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { + * check(!isActive) // attempting to suspend will throw + * println("3. An undispatched coroutine starts.") + * } + * ensureActive() // we can even crash the current coroutine. + * } * ``` * * This is a **delicate** API. The coroutine starts execution even if its [Job] is cancelled before starting. * However, the resources used within a coroutine may rely on the cancellation mechanism, * and cannot be used after the [Job] cancellation. For instance, in Android development, updating a UI element * is not allowed if the coroutine's scope, which is tied to the element's lifecycle, has been cancelled. - * - * Cancellability of coroutine at suspension points depends on the particular implementation details of - * suspending functions as in [DEFAULT]. */ @DelicateCoroutinesApi ATOMIC, /** - * Immediately executes the coroutine until its first suspension point _in the current thread_ similarly to - * the coroutine being started using [Dispatchers.Unconfined]. However, when the coroutine is resumed from suspension - * it is dispatched according to the [CoroutineDispatcher] in its context. + * Immediately executes the coroutine until its first suspension point _in the current thread_. * - * This is similar to [ATOMIC] in the sense that coroutine starts executing even if it was already cancelled, - * but the difference is that it starts executing in the same thread. + * Starting a coroutine using [UNDISPATCHED] is similar to using [Dispatchers.Unconfined] with [DEFAULT], except: + * - Resumptions from later suspensions will properly use the actual dispatcher from the coroutine's context. + * Only the code until the first suspension point will be executed immediately. + * - Even if the coroutine was cancelled already, its code will still start running, similar to [ATOMIC]. + * - The coroutine will not form an event loop. See [Dispatchers.Unconfined] for an explanation of event loops. * - * Cancellability of coroutine at suspension points depends on the particular implementation details of - * suspending functions as in [DEFAULT]. + * This set of behaviors makes [UNDISPATCHED] well-suited for cases where the coroutine has a distinct + * initialization phase whose side effects we want to rely on later. * - * ### Unconfined event loop + * Example: + * ``` + * var tasks = 0 + * repeat(3) { + * launch(start = CoroutineStart.UNDISPATCHED) { + * tasks++ + * try { + * println("Waiting for a reply...") + * delay(50.milliseconds) + * println("Got a reply!") + * } finally { + * tasks-- + * } + * } + * } + * // Because of UNDISPATCHED, + * // we know that the tasks already ran to their first suspension point, + * // so this number is non-zero initially. + * while (tasks > 0) { + * println("currently active: $tasks") + * delay(10.milliseconds) + * } + * ``` + * + * Here, we implement a publisher-subscriber interaction, where [UNDISPATCHED] ensures that the + * subscribers do get registered before the publisher first checks if it can stop emitting values due to + * the lack of subscribers. + * + * ``` + * // Constant usage of stack space + * fun CoroutineScope.factorialWithUnconfined(n: Int): Deferred = + * async(Dispatchers.Unconfined) { + * if (n > 0) { + * n * factorialWithUnconfined(n - 1).await() + * } else { + * 1 // replace with `error()` to see the stacktrace + * } + * } + * + * // Linearly increasing usage of stack space + * fun CoroutineScope.factorialWithUndispatched(n: Int): Deferred = + * async(start = CoroutineStart.UNDISPATCHED) { + * if (n > 0) { + * n * factorialWithUndispatched(n - 1).await() + * } else { + * 1 // replace with `error()` to see the stacktrace + * } + * } + * ``` + * + * Calling `factorialWithUnconfined` from this example will result in a constant-size stack, + * whereas `factorialWithUndispatched` will lead to `n` recursively nested calls, + * resulting in a stack overflow for large values of `n`. + * + * The behavior of [UNDISPATCHED] can be described with the following examples: + * + * ``` + * runBlocking { + * println("1. About to start a new coroutine.") + * launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) { + * println("2. The coroutine is immediately started in the same thread.") + * delay(10.milliseconds) + * println("4. The execution continues in a Dispatchers.Default thread.") + * } + * println("3. Execution of the outer coroutine only continues later.") + * } + * ``` + * + * ``` + * // Cancellation does not prevent the coroutine from being started + * runBlocking { + * println("1. First, we cancel this scope.") + * cancel() + * println("2. Now, we start a new UNDISPATCHED child.") + * launch(start = CoroutineStart.UNDISPATCHED) { + * check(!isActive) // the child is already cancelled + * println("3. We entered the coroutine despite being cancelled.") + * } + * println("4. Execution of the outer coroutine only continues later.") + * } + * ``` * - * Unlike [Dispatchers.Unconfined] and [MainCoroutineDispatcher.immediate], nested undispatched coroutines do not form - * an event loop that otherwise prevents potential stack overflow in case of unlimited nesting. + * **Pitfall**: unlike [Dispatchers.Unconfined] and [MainCoroutineDispatcher.immediate], nested undispatched + * coroutines do not form an event loop that otherwise prevents potential stack overflow in case of unlimited + * nesting. This property is necessary for the use case of guaranteed initialization, but may be undesirable in + * other cases. + * See [Dispatchers.Unconfined] for an explanation of event loops. */ UNDISPATCHED;