From f8c02060ab989916f6056c1488df13a831619cdc Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sat, 1 Jun 2024 09:13:26 +0200 Subject: [PATCH 01/17] Reduce duplication across the docs of the `CoroutineStart` entries All of them except `LAZY` mentioned that cancellability at suspension points depends on the specific suspending functions, and it looks like it applies to `LAZY`, so this information is moved to `CoroutineStart`'s top-level documentation. --- .../common/src/CoroutineStart.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 865be8e334..10f699e833 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -6,8 +6,13 @@ 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. * + * This parameter only affects how the coroutine behaves until it reaches the first suspension point. + * After that, cancellability and dispatching depend on the implementation details of the invoked suspending functions. + * Use [suspendCancellableCoroutine] to implement custom cancellable 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; @@ -27,9 +32,6 @@ public enum class CoroutineStart { * * 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. - * - * Cancellability of a coroutine at suspension points depends on the particular implementation details of - * suspending functions. Use [suspendCancellableCoroutine] to implement cancellable suspending functions. */ DEFAULT, @@ -67,9 +69,6 @@ public enum class CoroutineStart { * 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, @@ -82,9 +81,6 @@ public enum class CoroutineStart { * 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. * - * Cancellability of coroutine at suspension points depends on the particular implementation details of - * suspending functions as in [DEFAULT]. - * * ### Unconfined event loop * * Unlike [Dispatchers.Unconfined] and [MainCoroutineDispatcher.immediate], nested undispatched coroutines do not form From 5149c6d0cacbb1494b963d907edb1bbf5680797e Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sat, 1 Jun 2024 15:31:43 +0200 Subject: [PATCH 02/17] Remove a redundant `import` Presumably, the import statement was used to ensure that the symbols in the documentation are properly resolved. It does not seem to be necessary at the moment. --- kotlinx-coroutines-core/common/src/CoroutineStart.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 10f699e833..27686172c7 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -1,6 +1,5 @@ package kotlinx.coroutines -import kotlinx.coroutines.CoroutineStart.* import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* From 200e6b40ad89ea58617f4ac2eecc5f0e5dd53b73 Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sat, 1 Jun 2024 15:33:04 +0200 Subject: [PATCH 03/17] Extend the documentation for `CoroutineStart.LAZY` --- .../common/src/CoroutineStart.kt | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 27686172c7..fe7f128675 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -29,7 +29,7 @@ public enum class CoroutineStart { * 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]. * - * If coroutine [Job] is cancelled before it even had a chance to start executing, then it will not start its + * If the coroutine's [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. */ DEFAULT, @@ -37,11 +37,26 @@ public enum class CoroutineStart { /** * 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 [DEFAULT] happens in the thread that does it. * - * If coroutine [Job] is cancelled before it even had a chance to start executing, then it will not start its + * 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 even had a chance to start executing, then it will not start its * execution at all, but will complete with an exception. + * + * **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: + * ``` + * coroutineScope { + * launch(start = CoroutineStart.LAZY) { } + * } + * ``` */ LAZY, From 22bc930d86cc9f780d0f5a93c99a6f626c195d8b Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sun, 2 Jun 2024 10:04:18 +0200 Subject: [PATCH 04/17] Slightly reword the CoroutineStart documentation --- kotlinx-coroutines-core/common/src/CoroutineStart.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index fe7f128675..2b076e1119 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -6,17 +6,19 @@ 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 it reaches the first suspension point. * After that, cancellability and dispatching depend on the implementation details of the invoked suspending functions. * Use [suspendCancellableCoroutine] to implement custom cancellable 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] starts coroutine lazily, only when it is needed; + * - [ATOMIC] atomically (in a non-cancellable way) schedules the coroutine for execution according to its context; + * - [UNDISPATCHED] immediately executes the coroutine until its first suspension point _in the current thread_. */ public enum class CoroutineStart { /** From fc8e2ad9328c3ef758310c35c29eb8ec4181d067 Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sun, 2 Jun 2024 10:43:42 +0200 Subject: [PATCH 05/17] Extend the documentation for `CoroutineStart.DEFAULT` --- .../common/src/CoroutineStart.kt | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 2b076e1119..fef59f7980 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -22,17 +22,72 @@ import kotlin.coroutines.* */ 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. * - * 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. + * The behavior of [DEFAULT] depends on the result of [CoroutineDispatcher.isDispatchNeeded] in + * the context of the started coroutine. + * - In the typical case where a dispatch is needed, the coroutine is dispatched for execution on that dispatcher. + * It may take a while for the dispatcher to start the task; the thread that invoked the coroutine builder + * does not wait for the task to start and instead continues its execution. + * - 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]), + * the task is executed immediately in the same thread that invoked the coroutine builder, + * similarly to [UNDISPATCHED]. * - * 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]. + * If the coroutine's [Job] is cancelled before it started executing, then it will not start its + * execution at all, and will instead complete with an exception. * - * If the coroutine's [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. + * Comparisons with the other options: + * - [LAZY] delays the moment of the initial dispatch until the completion of the coroutine is awaited. + * - [ATOMIC] prevents the coroutine from being cancelled before its first suspension point. + * - [UNDISPATCHED] always executes the coroutine until the first suspension immediately in the same thread + * (as if [CoroutineDispatcher.isDispatchNeeded] returned `false`), + * and also, like [ATOMIC], it ensures that the coroutine cannot be cancelled before it starts executing. + * + * 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`. + * val job = launch { + * println("3. When the thread is available, we start the coroutine") + * } + * println("2. The thread keeps doing other work after launching the coroutine") + * } + * ``` + * + * ``` + * // 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. + * val job = launch(Dispatchers.Unconfined) { + * 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.") + * } + * ``` + * + * ``` + * // Example of cancelling coroutines before they start executing. + * runBlocking { + * launch { // dispatches the job to execute on this thread later + * 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, From 2549a230e43bcbce9bbed45ebc23ffde2bcd0e56 Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sun, 2 Jun 2024 11:06:10 +0200 Subject: [PATCH 06/17] Add samples to CoroutineStart.LAZY --- .../common/src/CoroutineStart.kt | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index fef59f7980..47b1a884d4 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -97,13 +97,13 @@ public enum class CoroutineStart { * 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 [DEFAULT] happens in the thread that does it. + * the dispatch procedure described in [DEFAULT] happens 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 even had a chance to start executing, then it will not start its - * execution at all, but will complete with an exception. + * If the coroutine's [Job] is cancelled before it started executing, then it will not start its + * execution at all, and will instead complete with an exception. * * **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. @@ -114,6 +114,39 @@ public enum class CoroutineStart { * launch(start = CoroutineStart.LAZY) { } * } * ``` + * + * Examples: + * + * ``` + * // 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 await 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.") + * } + * } + * ``` */ LAZY, From f6ca16ddb568ba2245d589965dafa06304cf4c50 Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sun, 2 Jun 2024 11:37:32 +0200 Subject: [PATCH 07/17] Extend the documentation for `CoroutineStart.ATOMIC` --- .../common/src/CoroutineStart.kt | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 47b1a884d4..f0ffa957a8 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -152,9 +152,42 @@ public enum class CoroutineStart { /** * 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 is similar to [DEFAULT], but the coroutine is guaranteed to start executing even if it was cancelled. + * This only affects the initial portion of the code: on subsequent suspensions, cancellation will work as usual. + * + * [UNDISPATCHED] also 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. + * + * Example: + * + * ``` + * // 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 will throw + * println("4. A coroutine that went through a dispatch also starts.") + * try { + * delay(10.milliseconds) + * 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 [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: From 613fac0d31ad2606a56b38fd78f12938f2fbcee4 Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sun, 2 Jun 2024 13:58:27 +0200 Subject: [PATCH 08/17] Extend the documentation for `CoroutineStart.UNDISPATCHED` --- .../common/src/CoroutineStart.kt | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index f0ffa957a8..8ea0f04d5a 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -211,17 +211,70 @@ public enum class CoroutineStart { 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 to be executed, similar to [ATOMIC]. * - * ### Unconfined event loop + * **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. * - * 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. + * ``` + * // 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`. + * + * Example of using [UNDISPATCHED]: + * + * ``` + * runBlocking { + * println("1. About to start a new coroutine.") + * val job = 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.") + * } + * ``` */ UNDISPATCHED; From ef2fba0c461cba8c51cf9e80521dcff296d51098 Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Sun, 2 Jun 2024 15:00:00 +0200 Subject: [PATCH 09/17] Add examples of when CoroutineStart.(ATOMIC|UNDISPATCHED) are used After playing around with CoroutineStart.LAZY, I failed to understand when it can be useful. --- .../common/src/CoroutineStart.kt | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 8ea0f04d5a..e66b4d1719 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -115,7 +115,7 @@ public enum class CoroutineStart { * } * ``` * - * Examples: + * Behavior of [LAZY] can be described with the following examples: * * ``` * // Example of lazily starting a new coroutine that goes through a dispatch @@ -160,7 +160,46 @@ public enum class CoroutineStart { * 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 N_PERMITS = 3 + * val semaphore = Semaphore(N_PERMITS) + * try { + * repeat(100) { + * semaphore.acquire() + * if (it != 7) { + * println("Scheduling $it...") + * } else { + * // "randomly" cancel the whole procedure + * cancel() + * } + * launch(Dispatchers.Default, start = CoroutineStart.ATOMIC) { + * println("Entered $it") + * try { + * println("Performing the procedure $it") + * delay(10.milliseconds) + * println("Done with the procedure $it") + * } finally { + * semaphore.release() + * } + * } + * } + * } finally { + * withContext(NonCancellable) { + * repeat(N_PERMITS) { semaphore.acquire() } + * println("All permits were successfully returned!") + * } + * } + * ``` + * + * Here, we used [ATOMIC] to ensure that a semaphore that was acquired outside of the coroutine does get released + * even if cancellation happens between `acquire()` and `launch`. + * As a result, the semaphore will eventually regain all three permits. + * + * Behavior of [ATOMIC] can be described with the following examples: * * ``` * // Example of cancelling atomically started coroutines @@ -218,6 +257,49 @@ public enum class CoroutineStart { * Only the code until the first suspension point will be executed immediately. * - Even if the coroutine was cancelled already, its code will still start to be executed, similar to [ATOMIC]. * + * 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. + * + * Example: + * ``` + * runBlocking { + * val channel = Channel(Channel.RENDEZVOUS) + * var subscribers = 0 + * fun CoroutineScope.awaitTickNumber(desiredTickNumber: Int) { + * launch(start = CoroutineStart.UNDISPATCHED) { + * ++subscribers + * try { + * for (tickNumber in channel) { + * if (tickNumber >= desiredTickNumber) { + * println("Tick number $desiredTickNumber reached") + * break + * } + * } + * } finally { + * --subscribers + * } + * } + * } + * for (subscriberIndex in 1..10) { + * awaitTickNumber(10 + subscriberIndex * 3) + * } + * // Send the current tick number every 10 milliseconds + * // while there are subscribers + * var i = 0 + * // Because of UNDISPATCHED, + * // we know that the subscribers are already initialized, + * // so this number is non-zero initially. + * while (subscribers > 0) { + * channel.trySend(++i) + * 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. + * * **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. @@ -248,7 +330,7 @@ public enum class CoroutineStart { * whereas `factorialWithUndispatched` will lead to `n` recursively nested calls, * resulting in a stack overflow for large values of `n`. * - * Example of using [UNDISPATCHED]: + * Behavior of [UNDISPATCHED] can be described with the following examples: * * ``` * runBlocking { From 74e3f0b9d49598f9511ecd72c84bfe598ebf7b35 Mon Sep 17 00:00:00 2001 From: globsterg <166155014+globsterg@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:43:35 +0300 Subject: [PATCH 10/17] Apply suggestions from code review Co-authored-by: Vsevolod Tolstopyatov --- kotlinx-coroutines-core/common/src/CoroutineStart.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index e66b4d1719..4db1c96ec6 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -12,11 +12,10 @@ import kotlin.coroutines.* * * This parameter only affects how the coroutine behaves until it reaches the first suspension point. * After that, cancellability and dispatching depend on the implementation details of the invoked suspending functions. - * Use [suspendCancellableCoroutine] to implement custom cancellable suspending functions. * * The summary of coroutine start options is: * - [DEFAULT] immediately schedules the coroutine for execution according to its context; - * - [LAZY] starts coroutine lazily, only when it is needed; + * - [LAZY] starts coroutine lazily, only when its result is needed; * - [ATOMIC] atomically (in a non-cancellable way) schedules the coroutine for execution according to its context; * - [UNDISPATCHED] immediately executes the coroutine until its first suspension point _in the current thread_. */ @@ -35,7 +34,7 @@ public enum class CoroutineStart { * similarly to [UNDISPATCHED]. * * If the coroutine's [Job] is cancelled before it started executing, then it will not start its - * execution at all, and will instead complete with an exception. + * execution at all, and will be considered [cancelled][Job.isCancelled]. * * Comparisons with the other options: * - [LAZY] delays the moment of the initial dispatch until the completion of the coroutine is awaited. @@ -126,7 +125,7 @@ public enum class CoroutineStart { * 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 await it.") + * println("2. The coroutine still has not started. Now, we join it.") * job.join() * } * ``` From 049b554e263a9fc3bcf7249fbe23dae54317d88c Mon Sep 17 00:00:00 2001 From: Awkin Globsterg Date: Fri, 28 Jun 2024 14:00:24 +0200 Subject: [PATCH 11/17] Address the review --- .../common/src/CoroutineStart.kt | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 4db1c96ec6..98e0f137fb 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -10,13 +10,14 @@ import kotlin.coroutines.* * [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 it reaches the first suspension point. + * This parameter only affects how the coroutine behaves until the code of its body starts executing. * After that, cancellability and dispatching depend on the implementation details of the invoked suspending functions. * * The summary of coroutine start options is: - * - [DEFAULT] immediately schedules the coroutine for execution according to its context; - * - [LAZY] starts coroutine lazily, only when its result is needed; - * - [ATOMIC] atomically (in a non-cancellable way) schedules the coroutine for execution according to its context; + * - [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 awaited. + * - [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 { @@ -34,14 +35,7 @@ public enum class CoroutineStart { * similarly to [UNDISPATCHED]. * * 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]. - * - * Comparisons with the other options: - * - [LAZY] delays the moment of the initial dispatch until the completion of the coroutine is awaited. - * - [ATOMIC] prevents the coroutine from being cancelled before its first suspension point. - * - [UNDISPATCHED] always executes the coroutine until the first suspension immediately in the same thread - * (as if [CoroutineDispatcher.isDispatchNeeded] returned `false`), - * and also, like [ATOMIC], it ensures that the coroutine cannot be cancelled before it starts executing. + * execution at all and will be considered [cancelled][Job.isCancelled]. * * Examples: * @@ -52,7 +46,7 @@ public enum class CoroutineStart { * // 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`. - * val job = launch { + * val job = launch { // CoroutineStart.DEFAULT is the 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") @@ -65,7 +59,7 @@ public enum class CoroutineStart { * println("1. About to start a coroutine not needing a dispatch.") * // Dispatch the job to execute. * // `Dispatchers.Unconfined` is explicitly chosen. - * val job = launch(Dispatchers.Unconfined) { + * val job = 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") @@ -77,7 +71,8 @@ public enum class CoroutineStart { * ``` * // Example of cancelling coroutines before they start executing. * runBlocking { - * launch { // dispatches the job to execute on this thread later + * // 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 @@ -102,7 +97,7 @@ public enum class CoroutineStart { * 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 instead complete with an exception. + * 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. @@ -153,9 +148,10 @@ public enum class CoroutineStart { * 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 is guaranteed to start executing even if it was cancelled. - * This only affects the initial portion of the code: on subsequent suspensions, cancellation will work as usual. + * This only affects the behavior until the body of the coroutine starts executing; + * inside the body, cancellation will work as usual. * - * [UNDISPATCHED] also ensures that coroutines will be started in any case. + * 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. * @@ -178,8 +174,9 @@ public enum class CoroutineStart { * launch(Dispatchers.Default, start = CoroutineStart.ATOMIC) { * println("Entered $it") * try { + * // this `try` block will be entered in any case because of ATOMIC * println("Performing the procedure $it") - * delay(10.milliseconds) + * delay(10.milliseconds) // may throw due to cancellation * println("Done with the procedure $it") * } finally { * semaphore.release() @@ -223,21 +220,6 @@ public enum class CoroutineStart { * } * ensureActive() // we can even crash the current coroutine. * } - * - * ``` - * - * 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: - * ``` - * fun CoroutineScope.producer(channel: SendChannel) = - * launch(start = CoroutineStart.ATOMIC) { - * try { - * // produce elements - * } finally { - * channel.close() - * } - * } * ``` * * This is a **delicate** API. The coroutine starts execution even if its [Job] is cancelled before starting. From bc48938d6cbb25f037789907609641874ba8a743 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 30 Jul 2024 12:35:39 +0200 Subject: [PATCH 12/17] Address the review further --- .../common/src/CoroutineStart.kt | 123 ++++++++---------- 1 file changed, 52 insertions(+), 71 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 98e0f137fb..77fa4e691e 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -11,11 +11,11 @@ import kotlin.coroutines.* * 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 depend on the implementation details of the invoked suspending functions. + * 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 the coroutine for execution according to its context. - * - [LAZY] delays the moment of the initial dispatch until the result of the coroutine is awaited. + * - [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_. @@ -46,7 +46,7 @@ public enum class CoroutineStart { * // 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`. - * val job = launch { // CoroutineStart.DEFAULT is the launch's default start mode + * 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") @@ -59,7 +59,7 @@ public enum class CoroutineStart { * println("1. About to start a coroutine not needing a dispatch.") * // Dispatch the job to execute. * // `Dispatchers.Unconfined` is explicitly chosen. - * val job = launch(Dispatchers.Unconfined) { // CoroutineStart.DEFAULT is the launch's default start mode + * 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") @@ -109,7 +109,7 @@ public enum class CoroutineStart { * } * ``` * - * Behavior of [LAZY] can be described with the following examples: + * The behavior of [LAZY] can be described with the following examples: * * ``` * // Example of lazily starting a new coroutine that goes through a dispatch @@ -160,52 +160,44 @@ public enum class CoroutineStart { * * Example: * ``` - * val N_PERMITS = 3 - * val semaphore = Semaphore(N_PERMITS) - * try { - * repeat(100) { - * semaphore.acquire() - * if (it != 7) { - * println("Scheduling $it...") - * } else { - * // "randomly" cancel the whole procedure - * cancel() - * } - * launch(Dispatchers.Default, start = CoroutineStart.ATOMIC) { - * println("Entered $it") - * try { - * // this `try` block will be entered in any case because of ATOMIC - * println("Performing the procedure $it") - * delay(10.milliseconds) // may throw due to cancellation - * println("Done with the procedure $it") - * } finally { - * semaphore.release() - * } - * } - * } - * } finally { - * withContext(NonCancellable) { - * repeat(N_PERMITS) { semaphore.acquire() } - * println("All permits were successfully returned!") + * val mutex = Mutex() + * + * mutex.lock() // lock the mutex outside the coroutine + * delay(10.milliseconds) // 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 semaphore that was acquired outside of the coroutine does get released * even if cancellation happens between `acquire()` and `launch`. * As a result, the semaphore will eventually regain all three permits. * - * Behavior of [ATOMIC] can be described with the following examples: + * The behavior of [ATOMIC] can be described with the following examples: * * ``` * // 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 will throw - * println("4. A coroutine that went through a dispatch also starts.") + * check(!isActive) // attempting to suspend later will throw + * println("4. The coroutine was cancelled (isActive = $isActive), but starts anyway.") * try { - * delay(10.milliseconds) + * 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.") @@ -243,37 +235,25 @@ public enum class CoroutineStart { * * Example: * ``` - * runBlocking { - * val channel = Channel(Channel.RENDEZVOUS) - * var subscribers = 0 - * fun CoroutineScope.awaitTickNumber(desiredTickNumber: Int) { - * launch(start = CoroutineStart.UNDISPATCHED) { - * ++subscribers - * try { - * for (tickNumber in channel) { - * if (tickNumber >= desiredTickNumber) { - * println("Tick number $desiredTickNumber reached") - * break - * } - * } - * } finally { - * --subscribers - * } + * 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-- * } * } - * for (subscriberIndex in 1..10) { - * awaitTickNumber(10 + subscriberIndex * 3) - * } - * // Send the current tick number every 10 milliseconds - * // while there are subscribers - * var i = 0 - * // Because of UNDISPATCHED, - * // we know that the subscribers are already initialized, - * // so this number is non-zero initially. - * while (subscribers > 0) { - * channel.trySend(++i) - * delay(10.milliseconds) - * } + * } + * // 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) * } * ``` * @@ -281,10 +261,6 @@ public enum class CoroutineStart { * subscribers do get registered before the publisher first checks if it can stop emitting values due to * the lack of subscribers. * - * **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. - * * ``` * // Constant usage of stack space * fun CoroutineScope.factorialWithUnconfined(n: Int): Deferred = @@ -311,12 +287,12 @@ public enum class CoroutineStart { * whereas `factorialWithUndispatched` will lead to `n` recursively nested calls, * resulting in a stack overflow for large values of `n`. * - * Behavior of [UNDISPATCHED] can be described with the following examples: + * The behavior of [UNDISPATCHED] can be described with the following examples: * * ``` * runBlocking { * println("1. About to start a new coroutine.") - * val job = launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) { + * 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.") @@ -338,6 +314,11 @@ public enum class CoroutineStart { * println("4. Execution of the outer coroutine only continues later.") * } * ``` + * + * **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. + * See [Dispatchers.Unconfined] for an explanation of event loops. */ UNDISPATCHED; From 404e155fc99e600844f31347c040f87141429d74 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 30 Jul 2024 14:16:20 +0200 Subject: [PATCH 13/17] Explain the typical dispatch procedure in CoroutineDispatcher docs --- .../common/src/CoroutineDispatcher.kt | 55 +++++++++++++++---- .../common/src/CoroutineStart.kt | 14 ++--- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 8a114f6ab7..bdc7dc849f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -6,23 +6,55 @@ 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 began 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]), + * 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 +237,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 +261,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 77fa4e691e..4e563ae256 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -24,15 +24,7 @@ public enum class CoroutineStart { /** * Immediately schedules the coroutine for execution according to its context. This is usually the default option. * - * The behavior of [DEFAULT] depends on the result of [CoroutineDispatcher.isDispatchNeeded] in - * the context of the started coroutine. - * - In the typical case where a dispatch is needed, the coroutine is dispatched for execution on that dispatcher. - * It may take a while for the dispatcher to start the task; the thread that invoked the coroutine builder - * does not wait for the task to start and instead continues its execution. - * - 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]), - * the task is executed immediately in the same thread that invoked the coroutine builder, - * similarly to [UNDISPATCHED]. + * [DEFAULT] uses the default dispatch procedure described in the [CoroutineDispatcher] documentation. * * 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]. @@ -91,7 +83,8 @@ public enum class CoroutineStart { * 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 [DEFAULT] happens in the thread that did it. + * 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]. @@ -229,6 +222,7 @@ public enum class CoroutineStart { * - 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 to be executed, similar to [ATOMIC]. + * - The coroutine will not form an event loop. See [Dispatchers.Unconfined] for an explanation of event loops. * * 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. From 448318754ea8451db56f2ae4f4d0b2c98385428a Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 1 Aug 2024 15:08:38 +0200 Subject: [PATCH 14/17] Mention that LAZY is often not the best choice --- .../common/src/CoroutineStart.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 4e563ae256..d1e3f5f46d 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -134,6 +134,28 @@ public enum class CoroutineStart { * } * } * ``` + * + * ## 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. + * + * 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, From 0a9b4ea93a0ccc73ef73bdf0cf21e92c82f3f1bb Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 2 Aug 2024 12:06:10 +0200 Subject: [PATCH 15/17] Fixes --- kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt | 3 ++- kotlinx-coroutines-core/common/src/CoroutineStart.kt | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index bdc7dc849f..458cd6e7ff 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -49,7 +49,8 @@ import kotlin.coroutines.* * but the [dispatch] method itself may return immediately, before the task has even began 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]), - * the coroutine is resumed in the thread performing the dispatch procedure, + * [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. * diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index d1e3f5f46d..ca6af508b2 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -178,7 +178,7 @@ public enum class CoroutineStart { * val mutex = Mutex() * * mutex.lock() // lock the mutex outside the coroutine - * delay(10.milliseconds) // initial portion of the work, protected by the mutex + * // ... // 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!") @@ -198,9 +198,9 @@ public enum class CoroutineStart { * } * ``` * - * Here, we used [ATOMIC] to ensure that a semaphore that was acquired outside of the coroutine does get released - * even if cancellation happens between `acquire()` and `launch`. - * As a result, the semaphore will eventually regain all three permits. + * Here, we used [ATOMIC] to ensure that a mutex that was acquired outside of 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: * From 5261f69f845d9358b321a9caa5cff977e5521c57 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 5 Aug 2024 13:06:57 +0200 Subject: [PATCH 16/17] Highlight lazy's behavior --- kotlinx-coroutines-core/common/src/CoroutineStart.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index ca6af508b2..e285dc6e94 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -148,6 +148,8 @@ public enum class CoroutineStart { * ``` * * 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: From 762a90b73b3ac7a568c316d9a910f045b66ab4a0 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 12 Aug 2024 13:19:42 +0200 Subject: [PATCH 17/17] Small fixes --- kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt | 2 +- kotlinx-coroutines-core/common/src/CoroutineStart.kt | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 458cd6e7ff..37b68760a0 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -46,7 +46,7 @@ import kotlin.coroutines.* * 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 began to execute. + * 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, diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index e285dc6e94..c4c4cea723 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -97,6 +97,7 @@ public enum class CoroutineStart { * 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) { } * } @@ -200,7 +201,7 @@ public enum class CoroutineStart { * } * ``` * - * Here, we used [ATOMIC] to ensure that a mutex that was acquired outside of the coroutine does get released + * 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. * @@ -245,7 +246,7 @@ public enum class CoroutineStart { * 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 to be executed, similar to [ATOMIC]. + * - 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. * * This set of behaviors makes [UNDISPATCHED] well-suited for cases where the coroutine has a distinct @@ -335,7 +336,8 @@ public enum class CoroutineStart { * * **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. + * 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;