diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index 23ef7665b5..9fdc6f0b47 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -17,29 +17,186 @@ import kotlin.jvm.* // --------------- launch --------------- /** - * Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job]. - * The coroutine is cancelled when the resulting job is [cancelled][Job.cancel]. + * Launches a new *child coroutine* of [CoroutineScope] without blocking the current thread + * and returns a reference to the coroutine as a [Job]. * - * The coroutine context is inherited from a [CoroutineScope]. Additional context elements can be specified with [context] argument. - * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. - * The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden - * with a corresponding [context] element. + * [block] is the computation of the new coroutine that will run concurrently. + * The coroutine is considered active until the block and all the child coroutines created in it finish. * - * By default, the coroutine is immediately scheduled for execution. - * Other start options can be specified via `start` parameter. See [CoroutineStart] for details. - * An optional [start] parameter can be set to [CoroutineStart.LAZY] to start coroutine _lazily_. In this case, - * the coroutine [Job] is created in _new_ state. It can be explicitly started with [start][Job.start] function - * and will be started implicitly on the first invocation of [join][Job.join]. + * [context] specifies the additional context elements for the coroutine to combine with + * the elements already present in the [CoroutineScope.coroutineContext]. + * It is incorrect to pass a [Job] element there, as this breaks structured concurrency. * - * Uncaught exceptions in this coroutine cancel the parent job in the context by default - * (unless [CoroutineExceptionHandler] is explicitly specified), which means that when `launch` is used with - * the context of another coroutine, then any uncaught exception leads to the cancellation of the parent coroutine. + * By default, the coroutine is scheduled for execution on its [ContinuationInterceptor]. + * There is no guarantee that it will start immediately: this is decided by the [ContinuationInterceptor]. + * It is possible that the new coroutine will be cancelled before starting, in which case its code will not be executed. + * The [start] parameter can be used to adjust this behavior. See [CoroutineStart] for details. * - * See [newCoroutineContext] for a description of debugging facilities that are available for a newly created coroutine. + * ## Structured Concurrency + * + * ### Coroutine context + * + * [launch] creates a *child coroutine* of `this` [CoroutineScope]. + * + * The context of the new coroutine is created like this: + * - First, the context of the [CoroutineScope] is combined with the [context] argument + * using the [newCoroutineContext] function. + * In most cases, this means that elements from [context] simply override + * the elements in the [CoroutineScope.coroutineContext]. + * If no [ContinuationInterceptor] is present in the resulting context, + * then [Dispatchers.Default] is added there. + * - Then, the [Job] in the [CoroutineScope.coroutineContext] is used as the *parent* of the new coroutine, + * unless overridden. + * Overriding the [Job] is forbidden; see a separate subsection below for details. + * The new coroutine's [Job] is added to the resulting context. + * + * The resulting coroutine context is the [coroutineContext] of the [CoroutineScope] + * passed to the [block] as its receiver. + * + * The new coroutine is considered [active][isActive] until the [block] and all its child coroutines finish. + * If the [block] throws a [CancellationException], the coroutine is considered cancelled, + * and if it throws any other exception, the coroutine is considered failed. + * + * ### Interactions between coroutines + * + * The details of structured concurrency are described in the [CoroutineScope] interface documentation. + * Here is a restatement of some main points as they relate to [launch]: + * + * - The lifecycle of the parent [CoroutineScope] can not end until this coroutine + * (as well as all its children) completes. + * - If the parent [CoroutineScope] is cancelled, this coroutine is cancelled as well. + * - If this coroutine fails with a non-[CancellationException] exception + * and the parent [CoroutineScope] has a non-supervisor [Job] in its context, + * the parent [Job] is cancelled with this exception. + * - If this coroutine fails with an exception and the parent [CoroutineScope] has a supervisor [Job] or no job at all + * (as is the case with [GlobalScope] or malformed scopes), + * the exception is considered uncaught and is propagated as the [CoroutineExceptionHandler] documentation describes. + * - The lifecycle of the [CoroutineScope] passed as the receiver to the [block] + * will not end until the [block] completes (or gets cancelled before ever having a chance to run). + * - If the [block] throws a [CancellationException], the coroutine is considered cancelled, + * cancelling all its children in turn, but the parent does not get notified. + * + * ### Overriding the parent job + * + * Passing a [Job] in the [context] argument breaks structured concurrency and is not a supported pattern. + * It does not throw an exception only for backward compatibility reasons, as a lot of code was written this way. + * Always structure your coroutines such that the lifecycle of the child coroutine is + * contained in the lifecycle of the [CoroutineScope] it is launched in. + * + * To help with migrating to structured concurrency, the specific behaviour of passing a [Job] in the [context] argument + * is described here. + * **Do not rely on this behaviour in new code.** + * + * If [context] contains a [Job] element, it will be the *parent* of the new coroutine, + * and the lifecycle of the new coroutine will not be tied to the [CoroutineScope] at all. + * + * In specific terms: + * + * - If the [CoroutineScope] is cancelled, the new coroutine will not be affected. + * - If the new coroutine fails with an exception, it will not cancel the [CoroutineScope]. + * Instead, the exception will be propagated to the [Job] passed in the [context] argument. + * If that [Job] is a [SupervisorJob], the exception will be unhandled, + * and will be propagated as the [CoroutineExceptionHandler] documentation describes. + * If that [Job] is not a [SupervisorJob], it will be cancelled with the exception thrown by [launch]. + * - If the [CoroutineScope] is lexically scoped (for example, created by [coroutineScope] or [withContext]), + * the function defining the scope will not wait for the new coroutine to finish. + * + * ## Communicating with the coroutine + * + * [Job.cancel] can be used to cancel the coroutine, and [Job.join] can be used to block until its completion + * without blocking the current thread. + * Note that [Job.join] succeeds even if the coroutine was cancelled or failed with an exception. + * [Job.cancelAndJoin] is a convenience function that combines cancellation and joining. + * + * If the coroutine was started with [start] set to [CoroutineStart.LAZY], the coroutine will not be scheduled + * to run on its [ContinuationInterceptor] immediately. + * [Job.start] can be used to start the coroutine explicitly, + * and awaiting its completion using [Job.join] also causes the coroutine to start executing. + * + * A coroutine created with [launch] does not return a result, and if it fails with an exception, + * there is no reliable way to learn about that exception in general. + * [async] is a better choice if the result of the coroutine needs to be accessed from another coroutine. + * + * ## Differences from [async] + * + * [launch] is similar to [async] whose block returns a [Unit] value. + * + * The only difference is the handling of uncaught coroutine exceptions: + * if an [async] coroutine fails with an exception, then even if the exception can not be propagated to the parent, + * a [CoroutineExceptionHandler] will not be invoked. + * Instead, the user of [async] must call [Deferred.await] to get the result of the coroutine, + * which will be the uncaught exception. + * + * ## Pitfalls + * + * ### [CancellationException] silently stopping computations + * + * ``` + * val deferred = GlobalScope.async { + * awaitCancellation() + * } + * deferred.cancel() + * coroutineScope { + * val job = launch { + * val result = deferred.await() + * println("Got $result") + * } + * job.join() + * println("Am I still not cancelled? $isActive") + * } + * ``` + * + * will output + * + * ``` + * Am I still not cancelled? true + * ``` + * + * This may be surprising, because the `launch`ed coroutine failed with an exception, + * but the parent still was not cancelled. + * + * The reason for this is that any [CancellationException] thrown in the coroutine is treated as a signal to cancel + * the coroutine, but not the parent. + * In this scenario, this is unlikely to be the desired behaviour: + * this was a failure and not a cancellation and should be propagated to the parent. + * + * This is a legacy behavior that cannot be changed in a backward-compatible way. + * Use [ensureActive] and [isActive] to distinguish between cancellation and failure: + * + * ``` + * launch { + * try { + * val result = deferred.await() + * } catch (e: CancellationException) { + * if (isActive) { + * // we were not cancelled, this is a failure + * println("`result` was cancelled") + * throw IllegalStateException("$result was cancelled", e) + * } else { + * println("I was cancelled") + * // throw again to finish the coroutine + * ensureActive() + * } + * } + * } + * ``` + * + * In simpler scenarios, this form can be used: + * + * ``` + * launch { + * try { + * // operation that may throw its own CancellationException + * } catch (e: CancellationException) { + * ensureActive() + * throw IllegalStateException(e) + * } + * } + * ``` * * @param context additional to [CoroutineScope.coroutineContext] context of the coroutine. * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. - * @param block the coroutine code which will be invoked in the context of the provided scope. + * @param block the coroutine code which will be invoked in the child coroutine. **/ public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, @@ -57,24 +214,73 @@ public fun CoroutineScope.launch( // --------------- async --------------- /** - * Creates a coroutine and returns its future result as an implementation of [Deferred]. - * The running coroutine is cancelled when the resulting deferred is [cancelled][Job.cancel]. - * The resulting coroutine has a key difference compared with similar primitives in other languages - * and frameworks: it cancels the parent job (or outer scope) on failure to enforce *structured concurrency* paradigm. - * To change that behaviour, supervising parent ([SupervisorJob] or [supervisorScope]) can be used. - * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. - * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. - * The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden - * with corresponding [context] element. - * - * By default, the coroutine is immediately scheduled for execution. - * Other options can be specified via `start` parameter. See [CoroutineStart] for details. - * An optional [start] parameter can be set to [CoroutineStart.LAZY] to start coroutine _lazily_. In this case, - * the resulting [Deferred] is created in _new_ state. It can be explicitly started with [start][Job.start] - * function and will be started implicitly on the first invocation of [join][Job.join], [await][Deferred.await] or [awaitAll]. - * - * @param block the coroutine code. + * Launches a new *child coroutine* of [CoroutineScope] without blocking the current thread + * and returns a reference to the coroutine as a [Deferred] that can be used to access the final value. + * + * [block] is the computation of the new coroutine that will run concurrently. + * The coroutine is considered active until the block and all the child coroutines created in it finish. + * The result of executing the [block] is available via the returned [Deferred]. + * + * [context] specifies the additional context elements for the coroutine to combine with + * the elements already present in the [CoroutineScope.coroutineContext]. + * It is incorrect to pass a [Job] element there, as this breaks structured concurrency. + * + * By default, the coroutine is scheduled for execution on its [ContinuationInterceptor]. + * There is no guarantee that it will start immediately: this is decided by the [ContinuationInterceptor]. + * It is possible that the new coroutine will be cancelled before starting, in which case its code will not be executed. + * The [start] parameter can be used to adjust this behavior. See [CoroutineStart] for details. + * + * ## Structured Concurrency + * + * ### Coroutine context + * + * [async] creates a *child coroutine* of `this` [CoroutineScope]. + * + * See the corresponding subsection in the [launch] documentation for details on how the coroutine context is created. + * In essence, the elements of [context] are combined with the elements of the [CoroutineScope.coroutineContext], + * typically overriding them. It is incorrect to pass a [Job] element there, as this breaks structured concurrency. + * + * ### Interactions between coroutines + * + * The details of structured concurrency are described in the [CoroutineScope] interface documentation. + * Here is a restatement of some main points as they relate to [async]: + * + * - The lifecycle of the parent [CoroutineScope] can not end until this coroutine + * (as well as all its children) completes. + * - If the parent [CoroutineScope] is cancelled, this coroutine is cancelled as well. + * - If this coroutine fails with a non-[CancellationException] exception + * and the parent [CoroutineScope] has a non-supervisor [Job] in its context, + * the parent [Job] is cancelled with this exception. + * - If this coroutine fails with an exception and the parent [CoroutineScope] has a supervisor [Job] or no job at all + * (as is the case with [GlobalScope] or malformed scopes), + * the exception is considered uncaught and is only available through the returned [Deferred]. + * - The lifecycle of the [CoroutineScope] passed as the receiver to the [block] + * will not end until the [block] completes (or gets cancelled before ever having a chance to run). + * - If the [block] throws a [CancellationException], the coroutine is considered cancelled, + * cancelling all its children in turn, but the parent does not get notified. + * + * ## Communicating with the coroutine + * + * [Deferred.await] can be used to suspend the current coroutine until the result of the [async] coroutine is available. + * It returns the result of the [block] executed in the [async] coroutine. + * Note that if the [async] coroutine fails with an exception, [Deferred.await] will also throw that exception, + * including [CancellationException] if the coroutine was cancelled. + * See the "CancellationException silently stopping computations" pitfall in the [launch] documentation. + * + * [Deferred.cancel] can be used to cancel the coroutine, and [Deferred.join] can be used to block until its completion + * without blocking the current thread or accessing its result. + * Note that [Deferred.join] succeeds even if the coroutine was cancelled or failed with an exception. + * [Deferred.cancelAndJoin] is a convenience function that combines cancellation and joining. + * + * If the coroutine was started with [start] set to [CoroutineStart.LAZY], the coroutine will not be scheduled + * to run on its [ContinuationInterceptor] immediately. + * [Deferred.start] can be used to start the coroutine explicitly, + * and awaiting its result using [Deferred.await] or [awaitAll] or its completion using [Deferred.join] + * also causes the coroutine to start executing. + * + * @param context additional to [CoroutineScope.coroutineContext] context of the coroutine. + * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. + * @param block the coroutine code which will be invoked in the child coroutine. */ public fun CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, @@ -114,28 +320,131 @@ private class LazyDeferredCoroutine( // --------------- withContext --------------- /** - * Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns + * Calls the specified suspending [block] with an updated coroutine context, suspends until it completes, and returns * the result. * - * The resulting context for the [block] is derived by merging the current [coroutineContext] with the - * specified [context] using `coroutineContext + context` (see [CoroutineContext.plus]). - * This suspending function is cancellable. It immediately checks for cancellation of - * the resulting context and throws [CancellationException] if it is not [active][CoroutineContext.isActive]. + * [context] specifies the additional context elements for the coroutine to combine with + * the elements already present in the [CoroutineScope.coroutineContext]. + * It is incorrect to pass a [Job] element there, as this breaks structured concurrency, + * unless it is [NonCancellable]. + * + * If the resulting [CoroutineScope.coroutineContext] is cancelled before the [block] starts running, + * [block] will immediately finish with a [CancellationException], + * possibly without even being scheduled for execution. + * + * ## Structured Concurrency + * + * The behavior of [withContext] is similar to [coroutineScope], as it, too, creates a new *scoped child coroutine*. + * Refer to the documentation of that function for details. + * + * The difference is that [withContext] does not simply call the [block] in a new coroutine + * but updates the [currentCoroutineContext] used for running it. + * + * The context of the new scope is created like this: + * - First, [currentCoroutineContext] is combined with the [context] argument. + * In most cases, this means that elements from [context] simply override + * the elements in the [currentCoroutineContext], + * but if they are `CopyableThreadContextElement`s, they are copied and combined as needed. + * - Then, the [Job] in the [currentCoroutineContext], if any, is used as the *parent* of the new scope, + * unless overridden. + * Overriding the [Job] is forbidden with the notable exception of [NonCancellable]; + * see a separate subsection below for details. + * The new scope's [Job] is added to the resulting context. + * + * The context of the new scope is obtained by combining the [currentCoroutineContext] with a new [Job] + * whose parent is the [Job] of the caller [currentCoroutineContext] (if any). + * The [Job] of the new scope is not a normal child of the caller coroutine but a lexically scoped one, + * meaning that the failure of the [Job] will not affect the parent [Job]. + * Instead, the exception leading to the failure will be rethrown to the caller of this function. + * + * ### Overriding the parent job + * + * #### [NonCancellable] * - * Calls to [withContext] whose [context] argument provides a [CoroutineDispatcher] that is - * different from the current one, by necessity, perform additional dispatches: the [block] - * can not be executed immediately and needs to be dispatched for execution on - * the passed [CoroutineDispatcher], and then when the [block] completes, the execution - * has to shift back to the original dispatcher. + * Passing [NonCancellable] in the [context] argument is a special case that allows + * the [block] to run even if the parent coroutine is cancelled. * - * Note that the result of `withContext` invocation is dispatched into the original context in a cancellable way + * This is useful in particular for performing cleanup operations + * if the cleanup procedure is itself a `suspend` function. + * + * Example: + * + * ``` + * class Connection { + * suspend fun terminate() + * } + * + * val connection = Connection() + * try { + * // some cancellable operations... + * } finally { + * withContext(NonCancellable) { + * // this block will run even if the parent coroutine is cancelled + * connection.terminate() + * } + * } + * ``` + * + * Beware that combining [NonCancellable] with context elements that change the dispatcher + * will make this cleanup code incorrect. See the [NonCancellable] documentation for details. + * + * #### Other [Job] elements + * + * Passing a [Job] in the [context] argument breaks structured concurrency and is not a supported pattern. + * It does not throw an exception only for backward compatibility reasons, as a lot of code was written this way. + * Always structure your coroutines such that the lifecycle of the child coroutine is + * contained in the lifecycle of the [CoroutineScope] it is launched in. + * + * To help with migrating to structured concurrency, the specific behaviour of passing a [Job] in the [context] argument + * is described here. + * **Do not rely on this behaviour in new code.** + * + * If [context] contains a [Job] element, it will be the *parent* of the new coroutine, + * and the lifecycle of the new coroutine will not be tied to the [CoroutineScope] at all. + * + * In specific terms: + * + * - If the [Job] passed to the [context] is cancelled, the new coroutine will be cancelled. + * - However, because [withContext] creates a scoped child coroutine, the failure of [block] + * will not cancel the parent [Job]. + * - If the [currentCoroutineContext] is cancelled, the new coroutine will not be affected. + * - In particular, if [withContext] avoided a dispatch (see "Dispatching behavior" below) + * and its block finished without an exception, [withContext] itself will not throw a [CancellationException]. + * This means that the block execution will continue even if the parent coroutine was cancelled. + * + * ## Dispatching behavior + * + * If [context] provides a [ContinuationInterceptor] other than the one used by the caller, + * the [block] can not simply be called inline with the updated context and has to go through a dispatch, + * that is, get scheduled on the new [ContinuationInterceptor]. + * It is up to the [ContinuationInterceptor] to actually run the [block], + * and it may take arbitrarily long for that to happen. + * After the [block] has completed, the computation has to be dispatched back to the original + * [ContinuationInterceptor]. + * + * If the resulting context in which [block] should run has the same [ContinuationInterceptor] + * as the caller, no dispatch is performed. + * In that case, no dispatch happens on exiting the [block] either, unless child coroutines have to be awaited. + * + * Note that the result of [withContext] invocation is dispatched into the original context in a cancellable way * with a **prompt cancellation guarantee**, which means that if the original [coroutineContext] - * in which `withContext` was invoked is cancelled by the time its dispatcher starts to execute the code, - * it discards the result of `withContext` and throws [CancellationException]. + * in which [withContext] was invoked is cancelled by the time its dispatcher starts to execute the code, + * it discards the result of [withContext] and throws a [CancellationException]. + * + * Note that if the dispatch from [withContext] back to the original context does not need to happen + * (because of having the same dispatcher and not having to wait for the children) + * *and* the [context] passed to [withContext] contains [NonCancellable], + * then cancellation of the caller will not prevent a value from being successfully returned. + * + * ## Pitfalls + * + * ### Returning closeable resources + * + * Values returned from [withContext] will typically be lost if the caller is cancelled. + * The exception is the `withContext(NonCancellable)` pattern. * - * The cancellation behaviour described above is enabled if and only if the dispatcher is being changed. - * For example, when using `withContext(NonCancellable) { ... }` there is no change in dispatcher and - * this call will not be cancelled neither on entry to the block inside `withContext` nor on exit from it. + * See the corresponding section in the [coroutineScope] documentation for details, + * as well as the [NonCancellable] documentation. */ public suspend fun withContext( context: CoroutineContext, diff --git a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt index 48e59fe3a9..2414f09a22 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt @@ -3,9 +3,18 @@ package kotlinx.coroutines import kotlin.coroutines.* /** - * Creates a context for a new coroutine. It installs [Dispatchers.Default] when no other dispatcher or - * [ContinuationInterceptor] is specified and adds optional support for debugging facilities (when turned on) - * and copyable-thread-local facilities on JVM. + * Creates a context for a new coroutine. + * + * This function is used by coroutine builders to create a new coroutine context. + * - It installs [Dispatchers.Default] when no other dispatcher or [ContinuationInterceptor] is specified. + * - On the JVM, if the debug mode is enabled, it assigns a unique identifier to every coroutine for tracking it. + * - On the JVM, copyable thread-local elements from [CoroutineScope.coroutineContext] and [context] + * are copied and combined as needed. + * - The elements of [context] and [CoroutineScope.coroutineContext] other than copyable thread-context ones + * are combined as is, with the elements from [context] overriding the elements from [CoroutineScope.coroutineContext] + * in case of equal [keys][CoroutineContext.Key]. + * + * See the documentation of this function's JVM implementation for platform-specific details. */ public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 9c1bd02728..7ea023b091 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -10,102 +10,481 @@ import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* /** - * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc.) - * is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext] - * to automatically propagate all its elements and cancellation. + * A scope in which coroutines run. * - * The best ways to obtain a standalone instance of the scope are [CoroutineScope()] and [MainScope()] factory functions, - * taking care to cancel these coroutine scopes when they are no longer needed (see the section on custom usage below for - * explanation and example). + * A scope defines a group of coroutines with shared execution properties + * (represented as [CoroutineContext.Element] values) + * and mutually dependent lifecycles. * - * Additional context elements can be appended to the scope using the [plus][CoroutineScope.plus] operator. + * The execution properties of coroutines are defined by the [coroutineContext] property of this interface + * and are inherited by all coroutines launched in this scope. + * For example, the [CoroutineDispatcher] element of the context defines which threads the coroutines will run on, + * with [Dispatchers.Main] meaning the coroutines will run on the main thread of the application. + * See a more detailed explanation of the context elements in a separate section below. * - * ### Convention for structured concurrency + * The lifecycles of coroutines are governed by a set of rules called "structured concurrency", + * meaning that the lifetimes of child coroutines are strictly inside the lifetime of the scope. + * If the scope is cancelled, all coroutines in it are cancelled too, and the scope itself + * cannot be completed until all its children are completed. + * See a more detailed explanation of structured concurrency in a separate section below. * - * Manual implementation of this interface is not recommended, implementation by delegation should be preferred instead. - * By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a - * [job][Job] to enforce the discipline of **structured concurrency** with propagation of cancellation. + * ## Using the coroutine scope * - * Every coroutine builder (like [launch], [async], and others) - * and every scoping function (like [coroutineScope] and [withContext]) provides _its own_ scope - * with its own [Job] instance into the inner block of code it runs. - * By convention, they all wait for all the coroutines inside their block to complete before completing themselves, - * thus enforcing the structured concurrency. See [Job] documentation for more details. + * The methods of this interface are not intended to be called directly. + * Instead, a [CoroutineScope] is passed as a receiver to the coroutine builders such as [launch] and [async], + * controlling the execution properties and lifetimes of the created coroutines. * - * ### Android usage + * ## Coroutine context elements * - * Android has first-party support for coroutine scope in all entities with the lifecycle. - * See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope). + * A [CoroutineScope] is defined by a set of [CoroutineContext] elements, one of which is typically a [Job], + * described in the section on structured concurrency and responsible for managing lifetimes of coroutines. * - * ### Custom usage + * Other coroutine context elements include, but are not limited to, the following: * - * `CoroutineScope` should be declared as a property on entities with a well-defined lifecycle that are - * responsible for launching child coroutines. The corresponding instance of `CoroutineScope` shall be created - * with either `CoroutineScope()` or `MainScope()`: + * - The scheduling policy, represented by a [CoroutineDispatcher] element. + * Some commonly used dispatchers are provided in the [Dispatchers] object. + * - [CoroutineExceptionHandler] that defines how failures of child coroutines should be reported whenever + * structured concurrency does not provide a way to propagate the failure to the parent + * (typically, because the root scope of the ancestry tree is not lexically scoped). + * - A [CoroutineName] element that can be used to name coroutines for debugging purposes. + * - On the JVM, a `ThreadContextElement` ensures that a specific thread-local value gets set on the thread + * that executes the coroutine. * - * - `CoroutineScope()` uses the [context][CoroutineContext] provided to it as a parameter for its coroutines - * and adds a [Job] if one is not provided as part of the context. - * - `MainScope()` uses [Dispatchers.Main] for its coroutines and has a [SupervisorJob]. + * ## Obtaining a coroutine scope * - * **The key part of custom usage of `CoroutineScope` is cancelling it at the end of the lifecycle.** - * The [CoroutineScope.cancel] extension function shall be used when the entity that was launching coroutines - * is no longer needed. It cancels all the coroutines that might still be running on behalf of it. + * Manual implementations of this interface are not recommended. + * Instead, there are several structured ways to obtain a [CoroutineScope]. * - * For example: + * ### Lexical scopes + * + * [coroutineScope] and [supervisorScope] functions can be used in any `suspend` function to define a scope + * lexically, ensuring that all coroutines launched in this scope have completed by the time scope-limiting + * function exits. + * + * ``` + * suspend fun doSomething() = coroutineScope { // scope `A` + * repeat(5) { outer -> + * // spawn a new coroutine in the scope `A` + * launch { + * println("Coroutine $outer started") + * coroutineScope { // scope `B`, separate for each `outer` coroutine + * repeat(5) { inner -> + * // spawn a new coroutine in the scope `B` + * launch { + * println("Coroutine $outer.$inner started") + * delay(10.milliseconds) + * println("Coroutine $outer.$inner finished") + * } + * } + * } + * // will only exit once all `Coroutine $outer.X finished` messages are printed + * println("Coroutine $outer finished") + * } + * } + * } // will only exit once all `Coroutine X finished` messages are printed + * ``` + * + * This should be the preferred way to create a scope for coroutines. + * + * ### `CoroutineScope` constructor function + * + * When the lifecycle of the scope is not limited lexically + * (for example, when coroutines should outlive the function that creates them) + * but is tied to the lifecycle of some entity, the [CoroutineScope] constructor function can be used + * to define a personal scope for that entity that should be stored as a field there. + * + * **The key part of using a custom `CoroutineScope` is cancelling it at the end of the lifecycle.** + * The [CoroutineScope.cancel] extension function shall be used when the entity launching coroutines + * is no longer needed. It cancels all the coroutines that might still be running on its behalf. + * + * ``` + * class MyEntity(scope: CoroutineScope? = null): AutoCloseable { + * // careful: do not write `get() =` here by accident! + * private val scope = scope ?: CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, e -> + * println("Error in coroutine: $e") + * }) + * + * fun doSomethingWhileEntityExists() = scope.launch { + * while (true) { + * // do some work + * delay(50.milliseconds) + * println("Doing something") + * } + * } + * + * override fun close() { + * // cancel all computations related to this entity + * scope.cancel() + * } + * } + * + * fun main() { + * MyEntity().use { entity -> + * entity.doSomethingWhileEntityExists() + * Thread.sleep(200) + * entity.close() + * } + * } + * ``` + * + * Usually, a custom [CoroutineScope] should be created with a [SupervisorJob] and + * a [CoroutineExceptionHandler] to handle exceptions in child coroutines. + * See the documentation for the [CoroutineScope] constructor function for more details. + * Also note that `MyEntity` accepts the `scope` parameter that can be used to pass a custom scope for testing. + * + * Sometimes, authors of coroutine-aware frameworks provide [CoroutineScope] instances like this out of the box. + * For example, on Android, all entities with a lifecycle and all `ViewModel` instances expose a [CoroutineScope]: + * see [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines). + * + * ### Taking another view of an existing scope + * + * Occasionally, several coroutines need to be launched with the same additional [CoroutineContext] that is not + * present in the original scope. + * In this case, the [CoroutineScope.plus] operator can be used to create a new view of the existing scope: + * + * ``` + * coroutineScope { + * val sameScopeButInUiThread = this + Dispatchers.Main + * sameScopeButInUiThread.launch { + * println("Running on the main thread") + * } + * launch { + * println("This will run using the original dispatcher") + * } + * sameScopeButInUiThread.launch { + * println("And this will also run on the main thread") + * } + * } + * ``` + * + * The lifecycle of the new scope is the same as the original one, but the context includes new elements. + * + * ### Application lifecycle scope + * + * [GlobalScope] is a [CoroutineScope] that has the lifetime of the whole application. + * It is convenient for launching top-level coroutines that are not tied to the lifecycle of any entity, + * but it is easy to misuse it and create memory leaks or resource leaks when a coroutine actually should be tied + * to the lifecycle of some entity. + * + * ``` + * GlobalScope.launch(CoroutineExceptionHandler { _, e -> + * println("Error in coroutine: $e") + * }) { + * while (true) { + * println("I will be running forever, you cannot stop me!") + * delay(1.seconds) + * } + * } + * ``` + * + * ### `by`-delegation + * + * When all else fails and a custom [CoroutineScope] implementation is needed, it is recommended to use + * `by`-delegation to implement the interface: + * + * ``` + * class MyEntity : CoroutineScope by CoroutineScope( + * SupervisorJob() + Dispatchers.Main + CoroutineExceptionHandler { _, e -> + * println("Error in coroutine: $e") + * } + * ) + * ``` + * + * ## Structured concurrency in detail + * + * ### Overview + * + * *Structured concurrency* is an approach to concurrent programming that attempts to clarify the lifecycles of + * concurrent operations and to make them easier to reason about. + * + * Skim the following motivating example: + * + * ``` + * suspend fun downloadFile(url: String): ByteArray { + * return withContext(Dispatchers.IO) { + * // this code will execute on the UI thread + * val file = byteArrayOf() + * // download the file + * return file + * } + * } + * + * suspend fun downloadAndCompareTwoFiles() { + * coroutineScope { + * val file1 = async { + * // if this fails, everything else quickly fails too + * downloadFile("http://example.com/file1") + * } + * val file2 = async { + * downloadFile("http://example.com/file2") + * } + * launch(Dispatchers.Main) { + * // create a separate coroutine on the UI thread + * if (file1.await() == file2.await()) { + * uiShow("Files are equal") + * } else { + * uiShow("Files are not equal") + * } + * } + * } + * // this line will only run once all the coroutines created above + * // finish their work or get cancelled + * } + * ``` + * + * In this example, two asynchronous operations are launched in parallel to download two files. + * If one of the files fails to download, the other one is cancelled too, and the whole operation fails. + * The `coroutineScope` function will not return until all the coroutines inside it are completed or cancelled. + * In addition, it is possible to cancel the coroutine calling `downloadAndCompareTwoFiles`, and all the coroutines + * inside it will be cancelled too. + * + * Without structured concurrency, ensuring that no resource leaks occur by the end of the operation and that + * the operation responds promptly to failure and cancellation requests is challenging. + * With structured concurrency, this orchestration is done automatically by the coroutine library, + * and it is enough to specify the relationships between operations declaratively, as shown in the example, + * without being overwhelmed by intricate inter-thread communications. + * + * ### Specifics + * + * Coroutines and [CoroutineScope] instances have an associated lifecycle. + * A runtime representation of a lifecycle in `kotlinx.coroutines` called a [Job]. + * [Job] instances form a hierarchy of parent-child relationships, + * and the [Job] of every coroutine spawned in a [CoroutineScope] is a child of the [Job] of that scope. + * This is often shortened to saying that the coroutine is the scope's child. + * + * See the [Job] documentation for a detailed explanation of the lifecycle stages. + * + * ``` + * coroutineScope { + * val job = coroutineContext[Job] + * val childJob = launch { } + * check(job === childJob.parent) + * } + * ``` + * + * Because every coroutine has a lifecycle and a [Job], a [CoroutineScope] can be associated with it. + * Most coroutine builders in `kotlinx.coroutines` expose the [CoroutineScope] of the coroutine on creation: + * + * ``` + * coroutineScope { // this block has a `CoroutineScope` receiver + * val parentScope = this + * var grandChildFinished = false + * val childJob = launch { + * // this block has a `CoroutineScope` receiver too + * val childScope = this + * check(childScope.coroutineContext[Job]?.parent + * === parentScope.coroutineContext[Job]) + * launch { + * // This block also has a `CoroutineScope` receiver! + * val grandChildScope = this + * check(grandChildScope.coroutineContext[Job]?.parent + * === childScope.coroutineContext[Job]) + * delay(100.milliseconds) + * grandChildFinished = true + * } + * // Because the grandchild coroutine + * // is a child of the child coroutine, + * // the child coroutine will not complete + * // until the grandchild coroutine does. + * } + * // Await completion of the child coroutine, + * // and therefore the grandchild coroutine too. + * childJob.join() + * check(grandChildFinished) + * } + * ``` + * + * Such a [CoroutineScope] receiver is provided for [launch], [async], and other coroutine builders, + * as well as for scoping functions like [coroutineScope], [supervisorScope], and [withContext]. + * Each of these [CoroutineScope] instances is tied to the lifecycle of the code block it runs in. + * + * Like the example above shows, a coroutine does not complete until all its children are completed. + * This means that [Job.join] on a [launch] or [async] result or [Deferred.await] on an [async] result + * will not return until all the children of that coroutine are completed, + * and scoping functions like [coroutineScope] and [withContext] will not return until all the coroutines + * launched in them are completed. + * + * #### Interactions between coroutines + * + * See the [Job] documentation for a detailed explanation of interactions between [Job] values. + * Below is a summary of the most important points for structuring code in day-to-day usage. + * + * A coroutine cannot reach the final state until all its children have reached their final states. + * See the example above. + * + * If a [CoroutineScope] is cancelled (either explicitly or because it corresponds to some coroutine that failed + * with an exception), all its children are cancelled too: + * + * ``` + * val scope = CoroutineScope( + * SupervisorJob() + CoroutineExceptionHandler { _, e -> } + * ) + * val job = scope.launch { + * // this coroutine will be cancelled + * awaitCancellation() + * } + * scope.cancel() // comment this out for the line below to hang + * job.join() // finishes normally + * ``` + * + * If a coroutine fails with a non-[CancellationException] exception, + * is not a coroutine created with lexically scoped coroutine builders like [coroutineScope] or [withContext], + * *and* its parent is a normal [Job] (not a [SupervisorJob]), + * the parent fails with that exception, too (and the same logic applies recursively to the parent of the parent, etc.): + * + * ``` + * check( + * runCatching { + * coroutineScope { + * launch { throw IllegalStateException() } + * launch { + * // this coroutine will be cancelled + * // when the parent gets cancelled + * awaitCancellation() + * } + * } + * }.exceptionOrNull() + * is IllegalStateException + * ) + * // the calling coroutine will *not* be cancelled, + * // even though the caller is a parent of the failed `coroutineScope`, + * // because `coroutineScope` is a lexically scoped coroutine builder, + * // which propagate their exceptions by rethrowing them. + * check(currentCoroutineContext().isActive) + * ``` + * + * Child jobs can lead to the failure of the parent even if the parent has already finished its work + * and was ready to return a value: * * ``` - * class MyUIClass { - * val scope = MainScope() // the scope of MyUIClass, uses Dispatchers.Main + * val deferred = GlobalScope.async { + * launch { + * delay(100.milliseconds) + * throw IllegalStateException() + * } + * 10 // this value will be lost! + * } + * check( + * runCatching { deferred.await() }.exceptionOrNull() + * is IllegalStateException + * ) + * ``` + * + * If several coroutines fail with non-[CancellationException] exceptions, + * the first one to fail will be propagated, and the rest will be attached to it as + * [suppressed exceptions][Throwable.suppressedExceptions]. + * + * If a coroutine fails with a non-[CancellationException] exception and cannot cancel its parent + * (because its parent is a [SupervisorJob] or there is none at all), + * the failure is reported through other channels. + * See [CoroutineExceptionHandler] for details. + * + * Failing with a [CancellationException] only cancels the coroutine itself and its children. + * It does not affect the parent or any other coroutines and is not considered a failure. + * + * ### How-to: stop failures of child coroutines from cancelling other coroutines + * + * If not affecting the [CoroutineScope] on a failure in a child coroutine is the desired behaviour, + * then a [SupervisorJob] should be used instead of [Job] when constructing the scope: * - * fun destroy() { // destroys an instance of MyUIClass - * scope.cancel() // cancels all coroutines launched in this scope - * // ... do the rest of cleanup here ... + * ``` + * val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineExceptionHandler { _, e -> + * println("Coroutine failed with exception $e") + * }) + * val failingCoroutine = scope.launch(Dispatchers.IO) { + * throw IllegalStateException("This exception will not cancel the scope") + * } + * failingCoroutine.join() + * scope.launch(Dispatchers.IO) { + * println("This coroutine is active! See: ${isActive}") + * } + * ``` + * + * Likewise, [supervisorScope] can replace [coroutineScope]: + * + * ``` + * supervisorScope { + * val failingCoroutine = scope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> + * println("Coroutine failed with exception $e") + * }) { + * throw IllegalStateException("This exception will not cancel the scope") + * } + * failingCoroutine.join() + * launch(Dispatchers.IO) { + * println("This coroutine is active! See: ${isActive}") * } + * } + * ``` * - * /* - * * Note: if this instance is destroyed or any of the launched coroutines - * * in this method throws an exception, then all nested coroutines are cancelled. - * */ - * fun showSomeData() = scope.launch { // launched in the main thread - * // ... here we can use suspending functions or coroutine builders with other dispatchers - * draw(data) // draw in the main thread + * ### How-to: prevent a child coroutine from being cancelled + * + * Sometimes, you may want to run a coroutine even if the parent coroutine is cancelled. + * This pattern provides a way to achieve that: + * + * ``` + * scope.launch(start = CoroutineStart.ATOMIC) { + * // Do not move `NonCancellable` to the `context` argument of `launch`! + * withContext(NonCancellable) { + * // This code will run even if the parent coroutine is cancelled * } * } * ``` + * + * [CoroutineStart.ATOMIC] ensures that the new coroutine is not cancelled until it is started. + * [NonCancellable] in [withContext] ensures that the code inside the block is executed even if the coroutine + * created by [launch] is cancelled. */ public interface CoroutineScope { /** * The context of this scope. - * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope. - * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages. * - * By convention, should contain an instance of a [job][Job] to enforce structured concurrency. + * The context represents various execution properties of the coroutines launched in this scope, + * such as the [dispatcher][CoroutineDispatcher] or + * the [procedure for handling uncaught exceptions][CoroutineExceptionHandler]. + * Except [GlobalScope], a [job][Job] instance for enforcing structured concurrency + * must also be present in the context of every [CoroutineScope]. + * See the documentation for [CoroutineScope] for details. + * + * Accessing this property in general code is not recommended for any purposes + * except accessing the [Job] instance for advanced usages. */ public val coroutineContext: CoroutineContext } /** - * Adds the specified coroutine context to this scope, overriding existing elements in the current - * scope's context with the corresponding keys. + * Creates a new view of `this` [CoroutineScope], but with the specified [context] added to it. * * This is a shorthand for `CoroutineScope(thisScope.coroutineContext + context)` and can be used as * a combinator with existing constructors: * ``` - * class MyActivity { - * val uiScope = MainScope() + CoroutineName("MyActivity") + * val myApplicationLifetimeScope = GlobalScope + CoroutineExceptionHandler { _, e -> + * myLogger.error("Error in coroutine: $e") * } * ``` + * + * **Pitfall**: if [context] contains a [Job], the new job will override the existing one in the scope. + * This means that in terms of structured concurrency, the new scope will be unrelated to the old one. + * Because of backward compatibility guarantees, [plus] does not check for this case, even though + * it is a source of bugs. */ public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope = ContextScope(coroutineContext + context) /** - * Creates the main [CoroutineScope] for UI components. + * Creates the a [CoroutineScope] for scheduling UI updates. * * Example of use: * ``` - * class MyAndroidActivity { - * private val scope = MainScope() + * class MyAndroidActivity: Activity() { + * // be careful not to write `get() =` here by accident! + * private val scope = MainScope() + CoroutineExceptionHandler { _, e -> + * log("Unhandled error in coroutine: $e") + * } + * + * fun updateProgressBar(newValue: Int) = scope.launch { + * // this coroutine will run on the main thread + * // ... + * } * * override fun onDestroy() { * super.onDestroy() @@ -114,28 +493,46 @@ public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineSco * } * ``` * + * The new scope has [Dispatchers.Main] in its context, meaning that all coroutines launched in it + * will run on the main thread. + * It also has a [SupervisorJob] in its context, meaning that if one of the child coroutines fails, + * the other coroutines will not be affected. + * A [CoroutineExceptionHandler] is not installed. + * * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements. - * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator: + * If you want to append additional elements to the main scope, use the [CoroutineScope.plus] operator: * `val scope = MainScope() + CoroutineName("MyActivity")`. + * + * **Pitfall**: this scope does not include a [CoroutineExceptionHandler] and creates a job without a parent. + * Together, this means that if a child coroutine created with [launch] fails with an exception, + * the failure will be reported in a platform-specific manner (e.g., a crash on Android). + * Always supply a [CoroutineExceptionHandler] to [MainScope] using the [plus] operator if there is a chance + * that a child coroutine may fail with an exception, + * or use [async] instead of [launch] to have the consumer of the result handle the exception. + * + * **Pitfall**: there is no memoization of the [CoroutineScope] instance in this function. + * Every call to this function creates a new instance of [MainScope], with an unrelated [SupervisorJob]. + * For example, writing `MainScope().cancel()` is meaningless, + * because the only job that will be cancelled is the one created in that specific `MainScope()` call. */ @Suppress("FunctionName") public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main) /** - * Returns `true` when the current [Job] is still active (has not completed and was not cancelled yet). + * Returns `true` when the [Job] of this [CoroutineScope] is still active (has not completed and was not cancelled yet). * - * Coroutine cancellation [is cooperative](https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative) - * and normally, it's checked if a coroutine is cancelled when it *suspends*, for example, + * Coroutine cancellation [is cooperative](https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative), + * and usually, it's checked if a coroutine is cancelled when it *suspends*, for example, * when trying to read from a [channel][kotlinx.coroutines.channels.Channel] that is empty. * - * Sometimes, a coroutine does not need to perform suspending operations, but still wants to be cooperative + * Sometimes, a coroutine does not need to perform suspending operations but still wants to be cooperative * and respect cancellation. * - * The [isActive] property is inteded to be used for scenarios like this: + * The [isActive] property is intended to be used for scenarios like this: * ``` * val watchdogDispatcher = Dispatchers.IO.limitParallelism(1) * fun backgroundWork() { - * println("Doing bookkeeping in the background in a non-suspending manner") + * println("Doing bookkeeping in the background in a blocking manner") * Thread.sleep(100L) // Sleep 100ms * } * // Part of some non-trivial CoroutineScope-confined lifecycle @@ -147,45 +544,124 @@ public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatch * } * ``` * - * This function returns `true` if there is no [job][Job] in the scope's [coroutineContext][CoroutineScope.coroutineContext]. - * This property is a shortcut for `coroutineContext.isActive` in the scope when - * [CoroutineScope] is available. - * See [coroutineContext][kotlin.coroutines.coroutineContext], - * [isActive][kotlinx.coroutines.isActive] and [Job.isActive]. + * This function returns `true` if the scope does not contain a [Job] in its + * [context][CoroutineScope.coroutineContext]. + * This can only happen for [GlobalScope] and malformed coroutine scopes. + * + * For checking if the [current coroutine context][currentCoroutineContext] and not some scope's context + * [is active][CoroutineContext.isActive], the following form can be used instead: + * + * ``` + * suspend fun tryDoSomething() { + * if (!currentCoroutineContext().isActive) return + * // ... + * } + * ``` + * + * @see CoroutineScope.ensureActive for a function that throws an exception instead of returning a boolean value. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") public val CoroutineScope.isActive: Boolean get() = coroutineContext[Job]?.isActive ?: true /** - * A global [CoroutineScope] not bound to any job. - * Global scope is used to launch top-level coroutines that operate - * throughout the application's lifetime and are not canceled prematurely. + * A [CoroutineScope] without any [Job]. + * + * The global scope is used to launch top-level coroutines whose lifecycles are not limited by structured concurrency. + * Since [GlobalScope] does not have a [Job], it is impossible to cancel all coroutines launched in it. + * Likewise, there is no way to wait for all coroutines launched in it to finish. * - * Active coroutines launched in `GlobalScope` do not keep the process alive. They are like daemon threads. + * Active coroutines launched in `GlobalScope` do not keep the process alive. + * They are similar to [daemon threads][https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setDaemon-boolean-] + * in this regard. * - * This is a **delicate** API. It is easy to accidentally create resource or memory leaks when - * `GlobalScope` is used. A coroutine launched in `GlobalScope` is not subject to the principle of structured - * concurrency, so if it hangs or gets delayed due to a problem (e.g., due to a slow network), it will stay working - * and consuming resources. For example, consider the following code: + * This is a **delicate** API. [GlobalScope] is easy to use to create new coroutines, + * avoiding all bureaucracy of structured concurrency, but it also means losing all its benefits. + * See the [CoroutineScope] documentation for a detailed explanation of structured concurrency and a list of ways + * to obtain a [CoroutineScope] most suitable for your use case. + * + * ## Pitfalls + * + * ### Computations can happen when they have no right to or are no longer needed + * + * Some computations must be scoped to the lifecycle of some entity. + * For example, after a user leaves a screen in a UI application, + * it no longer makes sense to obtain the data needed to display that screen, + * and attempting to update the UI may even crash the application. * * ``` - * fun loadConfiguration() { - * GlobalScope.launch { - * val config = fetchConfigFromServer() // network request - * updateConfiguration(config) + * GlobalScope.launch { + * val record = withContext(Dispatchers.IO) { + * fetchLastRecordFromServer() + * } + * withContext(Dispatchers.Main) { + * component.displayLastRecord(record) * } * } * ``` * - * A call to `loadConfiguration` creates a coroutine in the `GlobalScope` that works in the background without any - * provision to cancel it or to wait for its completion. If a network is slow, it keeps waiting in the background, - * consuming resources. Repeated calls to `loadConfiguration` will consume more and more resources. + * This code is incorrect because it does not take into account the lifecycle of the component: + * the whole computation should be cancelled if the component is destroyed. + * + * If a coroutine spawned in the [GlobalScope] actually is tied to a lifecycle of some entity to function correctly, + * the created coroutines must somehow be stored and then cancelled when the entity is destroyed. + * The easiest way to do this is to create a [CoroutineScope] using the constructor function, + * use it to launch the coroutines, and then cancel it when the entity is destroyed, + * which will also lead to the coroutines being cancelled. * - * ### Possible replacements + * ### Resource leaks * - * In many circumstances, uses of 'GlobalScope' should be removed, - * with the containing operation marked as 'suspend', for example: + * A coroutine that never gets cancelled or resumed can be a resource leak. + * + * ``` + * val requests = Channel() + * GlobalScope.launch { + * try { + * while (true) { + * val request = channel.receive() + * socket.send(request) + * } + * } finally { + * socket.close() + * } + * } + * ``` + * + * If at some point, everyone stops populating the channel, the coroutine will never resume or cancel, + * and the socket will not be closed, leading to a resource leak. + * + * Tying the coroutine to some lifecycle will ensure that the coroutine will get cancelled when its work + * is no longer needed, releasing all resources it holds. + * + * ### Crashes + * + * [GlobalScope] does not have a [CoroutineExceptionHandler] installed. + * This means that exceptions thrown in coroutines created using [launch] will be uncaught, + * leading to platform-specific behavior, such as crashing the application (on Android and Kotlin/Native) + * or populating the logs with potentially unnecessary information (on non-Android JVM, JS, and Wasm). + * Please see [CoroutineExceptionHandler] for details. + * + * ``` + * GlobalScope.launch { + * // depending on your platform, this line can crash your application + * throw IllegalStateException() + * } + * ``` + * + * One way to solve this would be to provide a [CoroutineExceptionHandler] in the [launch] arguments. + * However, it is often much simpler to use structured concurrency + * to ensure propagation of exceptions to the parent coroutine, + * which usually has a way to report them: for example, [coroutineScope] or `runBlocking` will rethrow the exception + * to the caller. + * + * ## How to create coroutines if not with a [GlobalScope]? + * + * [GlobalScope] is often used by beginners who do not see any other obvious way to create coroutines. + * This is an antipattern and should be avoided. In this section, a brief overview of the alternatives is provided. + * + * In many typical usage scenarios, it is not necessary to spawn new coroutines at all to run `suspend` functions. + * Several coroutines are needed only if the code is expected to run concurrently, + * and when the operation execute one after another, all `suspend` functions can be called sequentially: * * ``` * suspend fun loadConfiguration() { @@ -194,8 +670,15 @@ public val CoroutineScope.isActive: Boolean * } * ``` * - * In cases when `GlobalScope.launch` was used to launch multiple concurrent operations, the corresponding - * operations shall be grouped with [coroutineScope] instead: + * This requires that `loadConfiguration` is a `suspend` function, which is not always the case. + * Many coroutine-aware frameworks manage coroutine scopes for you and provide a way to call `suspend` functions + * (for example, by requiring you to provide a `suspend` lambda). + * Try to see if the function is already called from some other `suspend` function. + * If so, you can add the `suspend` modifier to this function and call it directly. + * + * If a [CoroutineScope] is necessary after all because several operations need to be run concurrently + * in the span of a single function call, + * the [coroutineScope] function can be used to create a new scope for the function: * * ``` * // concurrently load configuration and data @@ -207,28 +690,44 @@ public val CoroutineScope.isActive: Boolean * } * ``` * + * A common pattern is to use [withContext] in a top-level `suspend fun main()` function: + * + * ``` + * suspend fun main() { + * // choose the dispatcher on which the coroutines should run + * withContext(Dispatchers.Default) { + * // can call `suspend` functions here! + * } + * } + * ``` + * * In top-level code, when launching a concurrent operation from a non-suspending context, an appropriately - * confined instance of [CoroutineScope] shall be used instead of `GlobalScope`. See docs on [CoroutineScope] for - * details. + * confined instance of [CoroutineScope] shall be used instead of `GlobalScope`. + * + * See docs for the [CoroutineScope] interface + * for details on structured concurrency and an extensive list of ways to obtain a [CoroutineScope]. * * ### GlobalScope vs. Custom CoroutineScope * - * Do not replace `GlobalScope.launch { ... }` with `CoroutineScope().launch { ... }` constructor function call. - * The latter has the same pitfalls as `GlobalScope`. See [CoroutineScope] documentation on the intended usage of - * `CoroutineScope()` constructor function. + * Do not replace `GlobalScope.launch { ... }` with a `CoroutineScope().launch { ... }` constructor function call. + * The latter suffers from all the pitfalls described above. + * See the [CoroutineScope] documentation on the intended usage of the `CoroutineScope()` constructor function. * - * ### Legitimate use-cases + * ### Legitimate use cases * - * There are limited circumstances under which `GlobalScope` can be legitimately and safely used, such as top-level background - * processes that must stay active for the whole duration of the application's lifetime. Because of that, any use - * of `GlobalScope` requires an explicit opt-in with `@OptIn(DelicateCoroutinesApi::class)`, like this: + * There are limited circumstances under which `GlobalScope` can be legitimately and safely used, + * such as top-level background processes that must stay active for the whole duration of the application's lifetime. + * Because of that, any use of `GlobalScope` requires an explicit opt-in with `@OptIn(DelicateCoroutinesApi::class)`, + * like this: * * ``` * // A global coroutine to log statistics every second, must be always active * @OptIn(DelicateCoroutinesApi::class) - * val globalScopeReporter = GlobalScope.launch { + * val globalScopeReporter = GlobalScope.launch(CoroutineExceptionHandler) { _, e -> + * logFatalError("Error in the global statistics-logging coroutine: $e") + * }) { * while (true) { - * delay(1000) + * delay(1.seconds) * logStatistics() * } * } @@ -238,44 +737,121 @@ public val CoroutineScope.isActive: Boolean public object GlobalScope : CoroutineScope { /** * Returns [EmptyCoroutineContext]. + * + * Note that, unlike other [CoroutineScope] implementations, this scope does not have a [Job] in its context. */ override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext } /** - * Creates a [CoroutineScope] and calls the specified suspend block with this scope. - * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, using the - * [Job] from that context as the parent for a new [Job]. + * Runs the given [block] in-place in a new [CoroutineScope] based on the caller coroutine context, + * returning its result. + * + * The lifecycle of the new [Job] begins with starting the [block] and completes when both the [block] and + * all the coroutines launched in the scope complete. + * + * The context of the new scope is obtained by combining the [currentCoroutineContext] with a new [Job] + * whose parent is the [Job] of the caller [currentCoroutineContext] (if any). + * The [Job] of the new scope is not a normal child of the caller coroutine but a lexically scoped one, + * meaning that the failure of the [Job] will not affect the parent [Job]. + * Instead, the exception leading to the failure will be rethrown to the caller of this function. * - * This function is designed for _concurrent decomposition_ of work. When any child coroutine in this scope fails, - * this scope fails, cancelling all the other children (for a different behavior, see [supervisorScope]). - * This function returns as soon as the given block and all its child coroutines are completed. - * A usage of a scope looks like this: + * If any child coroutine in this scope fails with an exception, + * the scope fails, cancelling all the other children and its own [block]. + * See [supervisorScope] for a similar function that allows child coroutines to fail independently. + * + * Together, this makes [coroutineScope] suitable for representing a task + * that can be split into several subtasks, + * which can be executed concurrently but have their results combined at some point, + * all in the span of running a single function: * * ``` - * suspend fun showSomeData() = coroutineScope { - * val data = async(Dispatchers.IO) { // <- extension on current scope - * ... load some UI data for the Main thread ... + * // If the current coroutine is cancelled, `firstFile`, `secondFile`, + * // and `await()` get cancelled. + * fun downloadAndCompareTwoFiles() = coroutineScope { + * val firstFile = async { + * // If this fails, `secondFile` and `await()` get cancelled, + * // and `downloadAndCompareTwoFiles` rethrows the exception, + * // but does not cancel the calling coroutine, + * // giving it a chance to recover + * downloadFile("1.txt") + * } + * val secondFile = async { + * downloadFile("2.txt") * } + * firstFile.await().contentEquals(secondFile.await()) + * } + * ``` * - * withContext(Dispatchers.Main) { - * doSomeWork() - * val result = data.await() - * display(result) + * Rephrasing this in more practical terms, the specific list of structured concurrency interactions is as follows: + * - Cancelling the caller's [currentCoroutineContext] leads to cancellation of the new [CoroutineScope] + * (corresponding to the code running in the [block]), which in turn cancels all the coroutines launched in it. + * - If the new [CoroutineScope] fails with an exception + * (which happens if either its [block] or any child coroutine fails with an exception), + * the exception is rethrown to the caller, + * without directly affecting the caller's [Job]. + * Note that this happens on any child coroutine's failure even if [block] finishes successfully. + * - [coroutineScope] will only finish when all the coroutines launched in it finish. + * If all of them complete without failing, the [coroutineScope] returns the result of the [block] to the caller. + * + * 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. + * + * ## Pitfall: returning closeable resources from a scoped coroutine + * + * [R] must be a value that can safely be dropped. For example, this code is incorrect: + * + * ``` + * // DO NOT DO THIS + * val closeableResource = coroutineScope { + * // calculate the closeable resource somehow + * obtainResource() + * } + * closeableResource.use { resource -> + * // use the resource + * } + * ``` + * + * The problem is that, if the caller gets cancelled before [coroutineScope] completes, + * then even if the calculation of the closeable resource does not suspend at all, + * [coroutineScope] will throw [CancellationException] instead of returning any value. + * + * This pitfall applies to all [coroutineScope]-like functions, like [withContext], [withTimeout], or [supervisorScope]. + * For this discussion, we call them collectively `myLexicalScope`. + * + * If it is necessary to process a value returned from a lexical coroutine scope, the following pattern should be used: + * + * ``` + * var resource: MyResource? = null + * try { + * // note: do not simply return the resource here! + * myLexicalScope { + * resource = obtainResource() * } + * // the resource is available here + * } finally { + * resource?.close() * } * ``` * - * The scope in this example has the following semantics: - * 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI. - * 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception. - * 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled. - * 4) If the `async` block fails, `withContext` will be cancelled. + * If cancellation during the acquisition of the resource is also undesired, the following pattern can be used + * ([coroutineScope] is not needed here, as [withContext] defines its own [CoroutineScope], but : * - * The method may throw a [CancellationException] if the current job was cancelled externally, - * rethrow the exception thrown by [block], or throw an unhandled [Throwable] if there is one - * (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope). + * ``` + * withContext(NonCancellable) { + * myLexicalScope { + * obtainResource() + * } + * }.use { resource -> + * } + * ``` + * + * Be aware, however, that like any [NonCancellable] usage, this creates the risk of accessing values past the point + * where they are valid. + * For example, if the caller coroutine scope is tied to the lifecycle of a UI element, with cancellation meaning + * that the UI element was already disposed of, accessing it during the acquisition of a resource or + * before the first suspension point in [use] is not allowed and may lead to crashes. */ public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R { contract { @@ -288,11 +864,242 @@ public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R } /** - * Creates a [CoroutineScope] that wraps the given coroutine [context]. + * Creates a [CoroutineScope] with the given coroutine [context]. + * + * The provided [context] should contain a [Job], which will represent the lifecycle of the scope and become the parent + * of any coroutines launched in this scope. + * + * If a [Job] is not passed, a new `Job()` is created and added to the context. + * This is error-prone and should be avoided; it is only provided for backward compatibility. + * + * ## Intended usage + * + * ### Non-lexically-scoped [supervisorScope] + * + * The most common pattern of the `CoroutineScope()` builder function usage is to obtain a scope whose lifetime + * matches the lifetime of some object, with child coroutines performing operations on that object. + * Once the object gets destroyed, the coroutines in this scope must be cancelled. + * This is achieved with this pattern: + * + * ``` + * class ThingWithItsOwnLifetime(scope: CoroutineScope? = null): AutoCloseable { + * private val scope = scope ?: CoroutineScope( + * SupervisorJob() + Dispatchers.Main + + * CoroutineExceptionHandler { _, e -> + * // handle uncaught coroutine exceptions appropriately + * } + * ) + * + * fun doSomethingWithThing() = scope.launch { + * // this computation gets cancelled when the thing gets destroyed + * } + * + * override fun close() { + * // the computations should all stop running + * scope.cancel() + * } + * } + * ``` + * + * The `scope` parameter is needed to support injecting coroutine scopes for testing. + * + * ### Non-lexically-scoped [coroutineScope] + * + * An equivalent to [coroutineScope] represents a group of tasks that work together to achieve one goal and should + * succeed or fail together. + * + * ``` + * class SubtaskPool(scope: CoroutineScope? = null) { + * private val job = CompletableDeferred() + * private val scope = scope ?: CoroutineScope(job + Dispatchers.IO) + * + * fun addPieceOfWork() = scope.launch { + * // this coroutine will be cancelled when the pool is closed. + * // if this coroutine fails, the pool will fail too + * } + * + * fun cancel() { + * // this will cancel all the coroutines launched in this scope + * job.cancel() + * } + * + * suspend fun await() { + * job.complete(Unit) + * job.await() + * } + * } + * ``` + * + * ## Pitfalls + * + * ### No memoization + * + * Every call to this function creates a new instance of [CoroutineScope]. + * If a [Job] instance is passed in the [context], it is less efficient, but is not a bug, as then, + * the different [CoroutineScope] instances will still represent the same lifecycle and will be cancelled together. + * However, if the [context] does not contain a [Job], then every created [CoroutineScope] + * will be unrelated to the previous ones. + * + * ``` + * // // 1) ANTIPATTERN! DO NOT WRITE: creates an independent scope every time + * // val myScope: CoroutineScope get() = CoroutineScope(Dispatchers.Main) + * // // 2) Write this instead: + * // val myScope: CoroutineScope = CoroutineScope(Dispatchers.Main) + * myScope.launch { + * try { + * awaitCancellation() + * } finally { + * println("This line will only be printed in scenario 2)") + * } + * } + * myScope.cancel() + * ``` + * + * ### Forgetting to propagate or handle exceptions + * + * Every [CoroutineScope] without an explicit [Job] in its context is a potential source of crashes or degraded + * performance due to logging unhandled exceptions. + * `CoroutineScope(Dispatchers.Default).launch { error("") }` is enough to crash + * an Android or Kotlin/Native application. + * The reason for this is that the [Job] created for this scope does not have a parent, + * and therefore, there is no way to propagate the exception up the ancestry chain or to the caller of some function. + * + * One way to avoid this is to use a [CoroutineExceptionHandler] in the context of the scope: + * + * ``` + * val scope = CoroutineScope( + * Dispatchers.Default + + * CoroutineExceptionHandler { _, e -> + * // handle uncaught coroutine exceptions appropriately + * } + * ) + * + * scope.launch { + * error("This is okay") // this will not crash the application + * } + * ``` + * + * Another way is to provide a [Job] that knows how to propagate the exception. + * See [CoroutineExceptionHandler] for details of how exceptions are propagated. + * Here is an example of using a [CompletableDeferred] in the scope's [context] to properly handle exceptions: + * + * ``` + * val myDeferred = CompletableDeferred() + * try { + * val scope = CoroutineScope(myDeferred + Dispatchers.Default) + * scope.launch { + * error("This is okay") // this will not crash the application + * } + * } finally { + * // Do not forget to complete and await the deferred to check for exceptions! + * myDeferred.complete(Unit) + * // The exceptions from child coroutines will be thrown here: + * myDeferred.await() + * } + * ``` + * + * This specific example is not recommended, as it is easier to use [coroutineScope] to achieve a similar effect. + * However, it is more flexible, as it allows invoking the `finally` block separately. + * + * ### Surprising interactions between failing children + * + * If a [Job] is not passed in the context of this function explicitly, + * one is created using the `Job()` constructor function. + * As opposed to a [SupervisorJob], this [Job] will fail if any of its children fail. + * + * ``` + * val scope = CoroutineScope(Dispatchers.Main + CoroutineExceptionHandler { _, e -> + * println("Coroutine failed with exception $e") + * }) + * scope.launch { + * updateUI("Operation started!") + * // this coroutine will be cancelled + * // when the other coroutine fails + * delay(2.seconds) + * // this line will not be executed + * updateUI("Operation finished!") + * } + * scope.launch { + * error("This will cancel the other coroutines") + * } + * ``` + * + * This behavior is suitable for cases where one task is decomposed into several subtasks, + * and one failure means the whole operation can no longer succeed and will have to be cancelled. + * However, in the far more common scenarios where `CoroutineScope()` represents the lifecycle of some entity + * that supports multiple independent concurrent operations, + * this is not the desired behavior. + * + * Explicitly using a [SupervisorJob] in the context of the scope is the recommended way to avoid this. + * + * ### Unintentionally passing a [Job] in the context + * + * Sometimes, a [Job] is passed in the context of this function unintentionally, + * leading to unpredictable interactions between several scopes. + * + * Examples of this include `CoroutineScope(currentCoroutineContext())`, `CoroutineScope(coroutineContext)`, + * `CoroutineScope(scope.coroutineContext)`, or any variations thereof that add new elements or remove existing ones. + * + * In all of these cases, the [Job] in the context of the new scope will be used as the parent job of all the + * coroutines in the newly created scope, meaning this scope will essentially be a view of some other scope, + * similar to what the [CoroutineScope.plus] operation produces. + * + * ``` + * // ANTIPATTERN! DO NOT WRITE SUCH CODE + * suspend fun foo() { + * val scope = CoroutineScope(currentCoroutineContext()) + * scope.launch { + * delay(1.seconds) + * println("foo()'s child coroutine finished") + * } + * } + * + * suspend fun bar() { + * coroutineScope { + * foo() + * println("foo() finished, but the new coroutine is still running") + * } + * } + * ``` * - * If the given [context] does not contain a [Job] element, then a default `Job()` is created. - * This way, failure of any child coroutine in this scope or [cancellation][CoroutineScope.cancel] of the scope itself - * cancels all the scope's children, just like inside [coroutineScope] block. + * In this example, the new coroutine will be launched in the scope *of the caller*, + * and `foo()` will not await the completion of this coroutine. + * To await the completion, use [coroutineScope] instead: + * + * ``` + * suspend fun foo() = coroutineScope { + * launch { + * delay(1.seconds) + * println("foo()'s child coroutine finished") + * } + * } // `foo()` will only return when the child coroutine completes + * + * suspend fun bar() { + * coroutineScope { + * foo() + * println("foo() finished, along with its child coroutine") + * } + * } + * ``` + * + * If launching a coroutine in the context of the caller is the desired behavior, + * make it explicit by passing the context: + * + * ``` + * fun foo(scope: CoroutineScope) { + * scope.launch { + * delay(1.seconds) + * println("foo()'s child coroutine finished") + * } + * } + * + * suspend fun bar() { + * coroutineScope { + * foo(this) + * println("foo() created a child coroutine in `this` scope") + * } + * } + * ``` */ @Suppress("FunctionName") public fun CoroutineScope(context: CoroutineContext): CoroutineScope = @@ -300,9 +1107,114 @@ public fun CoroutineScope(context: CoroutineContext): CoroutineScope = /** * Cancels this scope, including its job and all its children with an optional cancellation [cause]. + * * A cause can be used to specify an error message or to provide other details on * a cancellation reason for debugging purposes. + * * Throws [IllegalStateException] if the scope does not have a job in it. + * + * This function is shorthand for `coroutineContext[Job]?.cancel(cause) ?: throw IllegalStateException()`. + * + * ## Usage + * + * ### Cancelling non-lexically scoped coroutines + * + * The primary purpose of this function is to cancel a manually created [CoroutineScope] when its lifecycle ends. + * + * ``` + * class ThingWithItsOwnLifetime(scope: CoroutineScope? = null): AutoCloseable { + * private val scope = scope ?: CoroutineScope( + * SupervisorJob() + Dispatchers.Main + + * CoroutineExceptionHandler { _, e -> + * // handle uncaught coroutine exceptions appropriately + * } + * ) + * + * override fun close() { + * // the computations should all stop running + * scope.cancel() + * } + * ``` + * + * Failing to cancel the scope when it is no longer needed will lead to resource leaks + * and can also potentially crash the program if some object used by the child coroutines becomes destroyed. + * + * ### Cancelling lexical [CoroutineScope] + * + * For lexically scoped coroutines, such as those created with [coroutineScope] or [withContext], + * canceling the scope explicitly is very rarely required. + * + * If no child coroutines were started, but it became obvious that the computation won't be needed, + * the scope can simply be exited: + * + * ``` + * coroutineScope { + * if (computationNotNeeded) return@coroutineScope + * // Start some child coroutines + * // ... + * } + * ``` + * + * Also, if the caller is cancelled, the scope will be cancelled as well, so there is no need to cancel it explicitly. + * + * [cancel] may be useful in cases when one child coroutine detects that the whole parent scope is no longer needed + * and wants to cancel it: + * + * ``` + * coroutineScope { + * val parentScope = this + * launch { + * val userRequest = getUserRequest() + * if (userRequest == TERMINATE) { + * parentScope.cancel() + * } + * // Do something with the request + * // ... + * } + * // Start other child coroutines... + * } + * ``` + * + * ## Pitfalls + * + * ### Cancelling oneself + * + * Cancelling a scope does not mean the computation finishes immediately, + * as coroutine cancellation [is cooperative](https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative). + * Only the next suspension point will observe the cancellation. + * For example: + * + * ``` + * try { + * coroutineScope { + * cancel() + * println("Will be printed anyway!") + * } + * } catch (e: CancellationException) { + * println( + * "The scope was cancelled " + + * "(the parent coroutine is still active: ${currentCoroutineContext().isActive})" + * ) + * throw e + * } + * ``` + * + * will output + * + * ``` + * Will be printed anyway! + * The scope was cancelled (the parent coroutine is still active: true) + * ``` + * + * Use `return` to immediately return from the scope after cancelling it: + * + * ``` + * coroutineScope { + * cancel() + * return@coroutineScope + * println("Will not be printed") + * } + * ``` */ public fun CoroutineScope.cancel(cause: CancellationException? = null) { val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this") @@ -310,23 +1222,24 @@ public fun CoroutineScope.cancel(cause: CancellationException? = null) { } /** - * Cancels this scope, including its job and all its children with a specified diagnostic error [message]. - * A [cause] can be specified to provide additional details on a cancellation reason for debugging purposes. - * Throws [IllegalStateException] if the scope does not have a job in it. + * Cancels this scope with a [CancellationException] with the given [message] and [cause]. + * + * Shorthand for `cancel(CancellationException(message, cause))`. */ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Unit = cancel(CancellationException(message, cause)) /** - * Throws the [CancellationException] that was the scope's cancellation cause if the scope is no longer [active][CoroutineScope.isActive]. + * Throws the [CancellationException] that was the scope's cancellation cause + * if the scope is no longer [active][CoroutineScope.isActive]. * - * Coroutine cancellation [is cooperative](https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative) + * Coroutine cancellation [is cooperative](https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative), * and normally, it's checked if a coroutine is cancelled when it *suspends*, for example, * when trying to read from a [channel][kotlinx.coroutines.channels.Channel] that is empty. * - * Sometimes, a coroutine does not need to perform suspending operations, but still wants to be cooperative + * Sometimes, a coroutine does not need to perform suspending operations but still wants to be cooperative * and respect cancellation. * - * [ensureActive] function is inteded to be used for these scenarios and immediately bubble up the cancellation exception: + * [ensureActive] function is intended to be used for these scenarios and immediately bubble up the cancellation exception: * ``` * val watchdogDispatcher = Dispatchers.IO.limitParallelism(1) * fun backgroundWork() { @@ -337,7 +1250,7 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni * // Part of some non-trivial CoroutineScope-confined lifecycle * launch(watchdogDispatcher) { * while (true) { - * // Repeatatively do some background work that is non-suspending + * // Repeatedly do some background work that is non-suspending * backgroundWork() * ensureActive() // Bail out if the scope was cancelled * postBackgroundCleanup() // Won't be invoked if the scope was cancelled @@ -346,17 +1259,28 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni * ``` * This function does not do anything if there is no [Job] in the scope's [coroutineContext][CoroutineScope.coroutineContext]. * - * @see CoroutineScope.isActive - * @see CoroutineContext.ensureActive + * For ensuring that the [current coroutine context][currentCoroutineContext] and not some scope's context + * [is active][CoroutineContext.isActive], the following form can be used instead: + * + * ``` + * suspend fun doSomething() { + * // throw if we are not allowed to suspend + * currentCoroutineContext().ensureActive() + * // ... + * } + * ``` + * + * @see CoroutineScope.isActive for a version that returns a boolean value instead of throwing an exception. */ public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive() /** * Returns the current [CoroutineContext] retrieved by using [kotlin.coroutines.coroutineContext]. + * * This function is an alias to avoid name clash with [CoroutineScope.coroutineContext]: * * ``` - * // ANTIPATTERN! DO NOT WRITE SUCH A CODE + * // ANTIPATTERN! DO NOT WRITE SUCH CODE * suspend fun CoroutineScope.suspendFunWithScope() { * // Name of the CoroutineScope.coroutineContext in 'this' position, same as `this.coroutineContext` * println(coroutineContext[CoroutineName]) @@ -366,10 +1290,21 @@ public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive() * * withContext(CoroutineName("Caller")) { * // Will print 'CoroutineName("Receiver")' and 'CoroutineName("Caller")' - * CoroutineScope("Receiver").suspendFunWithScope() + * CoroutineScope(CoroutineName("Receiver")).suspendFunWithScope() * } * ``` * - * This function should always be preferred over [kotlin.coroutines.coroutineContext] property even when there is no explicit clash. + * This function should always be preferred over [kotlin.coroutines.coroutineContext] property + * even when there is no explicit clash. + * + * Usage example: + * + * ``` + * // log an event while specifying the current coroutine + * println("[${currentCoroutineContext()[Job]}] Hello, World!") + * ``` + * + * Consider using [coroutineScope] instead of this function to obtain not only the current context + * but also a new [CoroutineScope] that allows easily launching coroutines and waiting for their completion. */ public suspend inline fun currentCoroutineContext(): CoroutineContext = coroutineContext diff --git a/kotlinx-coroutines-core/common/src/NonCancellable.kt b/kotlinx-coroutines-core/common/src/NonCancellable.kt index 25c4f6f9d9..eff67dda58 100644 --- a/kotlinx-coroutines-core/common/src/NonCancellable.kt +++ b/kotlinx-coroutines-core/common/src/NonCancellable.kt @@ -20,6 +20,150 @@ import kotlin.coroutines.* * if you write `launch(NonCancellable) { ... }` then not only the newly launched job will not be cancelled * when the parent is cancelled, the whole parent-child relation between parent and child is severed. * The parent will not wait for the child's completion, nor will be cancelled when the child crashed. + * + * ## Pitfalls + * + * ### Overriding the exception with a [CancellationException] in a finalizer + * + * #### Combining [NonCancellable] with a [ContinuationInterceptor] + * + * The typical usage of [NonCancellable] is to ensure that cleanup code is executed even if the parent job is cancelled. + * Example: + * + * ``` + * try { + * // some code using a resource + * } finally { + * withContext(NonCancellable) { + * // cleanup code that should not be cancelled + * } + * } + * ``` + * + * However, it is easy to get this pattern wrong if the cleanup code needs to run on some specific dispatcher: + * + * ``` + * // DO NOT DO THIS + * withContext(Dispatchers.Main) { + * try { + * // some code using a resource + * } finally { + * // THIS IS INCORRECT + * withContext(NonCancellable + Dispatchers.Default) { + * // cleanup code that should not be cancelled + * } // this line may throw a `CancellationException`! + * } + * } + * ``` + * + * In this case, if the parent job is cancelled, [withContext] will throw a [CancellationException] as soon + * as it tries to switch back from the [Dispatchers.Default] dispatcher back to the original one. + * The reason for this is that [withContext] obeys the **prompt cancellation** principle, + * which means that dispatching back from it to the original context will fail with a [CancellationException] + * even if the block passed to [withContext] finished successfully, + * overriding the original exception thrown by the `try` block, if any. + * + * To avoid this, you should use [NonCancellable] as the only element in the context of the `withContext` call, + * and then inside the block, you can switch to any dispatcher you need: + * + * ``` + * withContext(Dispatchers.Main) { + * try { + * // some code using a resource + * } finally { + * withContext(NonCancellable) { + * withContext(Dispatchers.Default) { + * // cleanup code that should not be cancelled + * } + * } + * } + * } + * ``` + * + * #### Launching child coroutines + * + * Child coroutines should not be started in `withContext(NonCancellable)` blocks in resource cleanup handlers directly. + * + * ``` + * // DO NOT DO THIS + * withContext(Dispatchers.Main) { + * try { + * // some code using a resource + * } finally { + * // THIS IS INCORRECT + * withContext(NonCancellable) { + * // cleanup code that should not be cancelled + * launch { delay(100.milliseconds) } + * } // this line may throw a `CancellationException`! + * } + * } + * ``` + * + * Similarly to the case of specifying a dispatcher alongside [NonCancellable] in a [withContext] argument, + * having to wait for child coroutines can lead to a dispatch at the end of the [withContext] call, + * which will lead to it throwing a [CancellationException] due to the prompt cancellation guarantee. + * + * The solution to this is also similar: + * + * ``` + * withContext(Dispatchers.Main) { + * try { + * // some code using a resource + * } finally { + * withContext(NonCancellable) { + * // note: `coroutineScope` here is required + * // to prevent a sporadic CancellationException + * coroutineScope { + * // cleanup code that should not be cancelled + * launch { delay(100.milliseconds) } + * } + * } + * } + * } + * ``` + * + * Because now [coroutineScope] and not [withContext] has to wait for the children, there is once again no dispatch + * between the last line of the [withContext] block and getting back to the caller. + * + * ### Not reacting to cancellations right outside the [withContext] + * + * Just like combining [NonCancellable] with other elements is incorrect because cancellation may override + * the original exception, the opposite can also be incorrect, depending on the context: + * + * ``` + * // DO NOT DO THIS + * withContext(Dispatchers.Main) { + * withContext(NonCancellable) { + * withContext(Dispatchers.Default) { + * // do something + * } + * } // will not react to the caller's cancellation! + * // BUG HERE + * updateUi() // may be invoked when the caller is already cancelled + * } + * ``` + * + * Here, the following may happen: + * 1. The `do something` block gets entered, and the main thread gets released and is free to perform other tasks. + * 2. Some other task updates the UI and cancels this coroutine, which is no longer needed. + * 3. `do something` finishes, and the computation is dispatched back to the main thread. + * 4. `updateUi()` is called, even though the coroutine was already cancelled and the UI is no longer in a valid state + * for this update operation, potentially leading to a crash. + * + * [ensureActive] can be used to manually ensure that cancelled code no longer runs: + * + * ``` + * withContext(Dispatchers.Main) { + * withContext(NonCancellable) { + * withContext(Dispatchers.Default) { + * // do something + * } + * } + * ensureActive() // check if we are still allowed to run the code + * updateUi() + * } + * ``` + * */ @OptIn(InternalForInheritanceCoroutinesApi::class) @Suppress("DeprecatedCallableAddReplaceWith") diff --git a/kotlinx-coroutines-core/common/src/Supervisor.kt b/kotlinx-coroutines-core/common/src/Supervisor.kt index deec713e86..94fffa815d 100644 --- a/kotlinx-coroutines-core/common/src/Supervisor.kt +++ b/kotlinx-coroutines-core/common/src/Supervisor.kt @@ -33,19 +33,74 @@ public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobIm public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent) /** - * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend [block] with this scope. - * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, using the - * [Job] from that context as the parent for the new [SupervisorJob]. - * This function returns as soon as the given block and all its child coroutines are completed. + * Runs the given [block] in-place in a new [CoroutineScope] with a [SupervisorJob] + * based on the caller coroutine context, returning its result. * - * Unlike [coroutineScope], a failure of a child does not cause this scope to fail and does not affect its other children, - * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for additional details. + * The lifecycle of the new [SupervisorJob] begins with starting the [block] and completes when both the [block] and + * all the coroutines launched in the scope complete. * - * If an exception happened in [block], then the supervisor job is failed and all its children are cancelled. - * If the current coroutine was cancelled, then both the supervisor job itself and all its children are cancelled. + * The context of the new scope is obtained by combining the [currentCoroutineContext] with a new [SupervisorJob] + * whose parent is the [Job] of the caller [currentCoroutineContext] (if any). + * The [SupervisorJob] of the new scope is not a normal child of the caller coroutine but a lexically scoped one, + * meaning that the failure of the [SupervisorJob] will not affect the parent [Job]. + * Instead, the exception leading to the failure will be rethrown to the caller of this function. * - * The method may throw a [CancellationException] if the current job was cancelled externally, - * or rethrow an exception thrown by the given [block]. + * If a child coroutine launched in the new scope fails, it will not affect the other children of the scope. + * However, if the [block] finishes with an exception, it will cancel the scope and all its children. + * See [coroutineScope] for a similar function that treats every child coroutine as crucial for obtaining the result + * and cancels the whole computation if one of them fails. + * + * Together, this makes [supervisorScope] a good choice for launching multiple coroutines where some failures + * are acceptable and should not affect the others. + * + * ``` + * // cancelling the caller's coroutine will cancel the new scope and all its children + * suspend fun tryDownloadFiles(urls: List): List> = + * supervisorScope { + * urls.map { url -> + * async { + * // if one of the downloads fails, the others will continue + * donwloadFileContent(url) + * } + * } + * } // every download will fail or complete by the time this function returns + * ``` + * + * Rephrasing this in more practical terms, the specific list of structured concurrency interactions is as follows: + * - Cancelling the caller's [currentCoroutineContext] leads to cancellation of the new [CoroutineScope] + * (corresponding to the code running in the [block]), which in turn cancels all the coroutines launched in it. + * - If the [block] fails with an exception, the exception is rethrown to the caller, + * without directly affecting the caller's [Job]. + * - [supervisorScope] will only finish when all the coroutines launched in it finish. + * After that, the [supervisorScope] returns (or rethrows) the result of the [block] to the caller. + * + * 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. + * + * ## Pitfalls + * + * ### Uncaught exceptions in child coroutines + * + * [supervisorScope] does not install a [CoroutineExceptionHandler] in the new scope. + * This means that if a child coroutine started with [launch] fails, its exception will be unhandled, + * possibly crashing the program. Use the following pattern to avoid this: + * + * ``` + * withContext(CoroutineExceptionHandler { _, exception -> + * // handle the exceptions as needed + * }) { + * supervisorScope { + * // launch child coroutines here + * } + * } + * ``` + * + * Alternatively, the [CoroutineExceptionHandler] can be supplied to the newly launched coroutines themselves. + * + * ### Returning closeable resources + * + * Values returned from [supervisorScope] will be lost if the caller is cancelled. + * See the corresponding section in the [coroutineScope] documentation for details. */ public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R): R { contract { diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 65e68ba299..c6e2ceb4cc 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -46,15 +46,11 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco } /** - * Runs a given suspending [block] of code inside a coroutine with the specified [timeout] and throws - * a [TimeoutCancellationException] if the timeout was exceeded. - * If the given [timeout] is non-positive, [TimeoutCancellationException] is thrown immediately. - * - * The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of - * the cancellable suspending function inside the block throws a [TimeoutCancellationException]. + * Calls the specified suspending [block] with the specified [timeout], suspends until it completes, + * and returns the result. * - * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. - * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. + * If the [block] execution times out, it is cancelled with a [TimeoutCancellationException]. + * If the [timeout] is non-positive, this happens immediately and the [block] is not executed. * * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, * even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some @@ -64,6 +60,97 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco * section of the coroutines guide for details. * * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * + * ## Structured Concurrency + * + * [withTimeout] behaves like [coroutineScope], as it, too, creates a new *scoped child coroutine*. + * Refer to the documentation of [coroutineScope] for details. + * + * ## Pitfalls + * + * ### Cancellation is cooperative + * + * [withTimeout] will not automatically stop all code inside it from being executed once the timeout gets triggered. + * It only cancels the running [block], but it's up to the [block] to notice that it was cancelled, for example, + * using [ensureActive], checking [isActive], or using [suspendCancellableCoroutine]. + * + * For example, this JVM code will run to completion, taking 10 seconds to do so: + * + * ``` + * withTimeout(1.seconds) { + * Thread.sleep(10_000) + * } + * ``` + * + * On the JVM, use the `runInterruptible` function to propagate cancellations + * to blocking JVM code as thread interruptions. + * + * See the [Cancellation is cooperative](https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative). + * section of the coroutines guide for details. + * + * ### [TimeoutCancellationException] is not considered an error + * + * Consider this code: + * + * ``` + * coroutineScope { + * launch { + * withTimeout(10.milliseconds) { + * // Some operation that is going to time out + * awaitCancellation() + * } + * } + * } + * ``` + * + * Here, the timeout will be triggered, and [withTimeout] will finish with a [TimeoutCancellationException]. + * However, [coroutineScope] will finish normally. + * The reason is that when coroutines finish with a [CancellationException], + * the error does not get propagated to the parent, just like it doesn't when a child actually gets cancelled. + * + * For ensuring that timeouts are treated as true errors that should cause the parent to fail, + * use [withTimeoutOrNull] and check the return value: + * + * ``` + * coroutineScope { + * launch { + * withTimeoutOrNull(10.milliseconds) { + * // Some operation that is going to time out + * awaitCancellation() + * } ?: error("Timed out!") + * } + * } + * ``` + * + * If [withTimeout] has to return a nullable value and [withTimeoutOrNull] can not be used, + * this pattern can help instead: + * + * ``` + * coroutineScope { + * launch { + * try { + * withTimeoutOrNull(10.milliseconds) { + * // Some operation that is going to time out + * awaitCancellation() + * } + * } catch (e: TimeoutCancellationException) { + * error("Timed out!") + * } + * } + * } + * ``` + * + * Another option is to specify the timeout action in a [select] invocation + * with [onTimeout][SelectBuilder.onTimeout] clause. + * + * ### Returning closeable resources + * + * Values returned from [withTimeout] will typically be lost if the caller is cancelled. + * + * See the corresponding section in the [coroutineScope] documentation for details. + * + * @see withTimeoutOrNull + * @see SelectBuilder.onTimeout */ public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T { contract { diff --git a/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt index 6fd11ab107..3cb7a616de 100644 --- a/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt +++ b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt @@ -12,10 +12,80 @@ import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName /** - * Runs a new coroutine and **blocks** the current thread until its completion. + * Runs the given [block] in-place in a new coroutine based on [context], + * **blocking the current thread** until its completion, and then returning its result. * * It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in - * `main` functions and in tests. + * `main` functions, in tests, and in non-`suspend` callbacks when `suspend` functions need to be called. + * + * On the JVM, if this blocked thread is interrupted (see `java.lang.Thread.interrupt`), + * then the coroutine job is cancelled and this `runBlocking` invocation throws an `InterruptedException`. + * On Kotlin/Native, there is no way to interrupt a thread. + * + * ## Structured concurrency + * + * The lifecycle of the new coroutine's [Job] begins with starting the [block] and completes when both the [block] and + * all the coroutines launched in the scope complete. + * + * A new coroutine is created with the following properties: + * - A new [Job] for a lexically scoped coroutine is created. + * Its parent is the [Job] from the [context], if any was passed. + * - If a [ContinuationInterceptor] is passed in [context], + * it is used as a dispatcher of the new coroutine created by [runBlocking]. + * Otherwise, the new coroutine is dispatched to an event loop opened on this thread. + * - The other pieces of the context are put into the new coroutine context as is. + * - [newCoroutineContext] is called to optionally install debugging facilities. + * + * The resulting context is available in the [CoroutineScope] passed as the [block]'s receiver. + * + * Because the new coroutine is lexically scoped, even if a [Job] was passed in the [context], + * it will not be cancelled if [runBlocking] or some child coroutine fails with an exception. + * Instead, the exception will be rethrown to the caller of this function. + * + * If any child coroutine in this scope fails with an exception, + * the scope fails, cancelling all the other children and its own [block]. + * If children should fail independently, consider using [supervisorScope]: + * ``` + * runBlocking(CoroutineExceptionHandler { _, e -> + * // handle the exception + * }) { + * supervisorScope { + * // Children fail independently here + * } + * } + * ``` + * + * Rephrasing this in more practical terms, the specific list of structured concurrency interactions is as follows: + * - The caller's [currentCoroutineContext] *is not taken into account*, its cancellation does not affect [runBlocking]. + * - If the new [CoroutineScope] fails with an exception + * (which happens if either its [block] or any child coroutine fails with an exception), + * the exception is rethrown to the caller, + * without affecting the [Job] passed in the [context] (if any). + * Note that this happens on any child coroutine's failure even if [block] finishes successfully. + * - Cancelling the [Job] passed in the [context] (if any) cancels the new coroutine and its children. + * - [runBlocking] will only finish when all the coroutines launched in it finish. + * If all of them complete without failing, the [runBlocking] returns the result of the [block] to the caller. + * + * ## Event loop + * + * The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes + * continuations in this blocked thread until the completion of this coroutine. + * + * This event loop is set in a thread-local variable and is accessible to nested [runBlocking] calls and + * coroutine tasks forming an event loop + * (such as the tasks of [Dispatchers.Unconfined] and [MainCoroutineDispatcher.immediate]). + * + * Nested [runBlocking] calls may execute other coroutines' tasks instead of running their own tasks. + * + * When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of + * the specified dispatcher while the current thread is blocked (and possibly running tasks from other + * [runBlocking] calls on the same thread or [Dispatchers.Unconfined]). + * + * See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`. + * + * ## Pitfalls + * + * ### Calling from a suspend function * * Calling [runBlocking] from a suspend function is redundant. * For example, the following code is incorrect: @@ -25,27 +95,72 @@ import kotlin.jvm.JvmName * val data = runBlocking { // <- redundant and blocks the thread, do not do that * fetchConfigurationData() // suspending function * } + * // ... * ``` * * Here, instead of releasing the thread on which `loadConfiguration` runs if `fetchConfigurationData` suspends, it will * block, potentially leading to thread starvation issues. + * Additionally, the [currentCoroutineContext] will be ignored, and the new computation will run in the context of + * the new `runBlocking` coroutine. * - * The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations - * in this blocked thread until the completion of this coroutine. - * See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`. + * Instead, write it like this: * - * When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of - * the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`, - * then this invocation uses the outer event loop. + * ``` + * suspend fun loadConfiguration() { + * val data = fetchConfigurationData() // suspending function + * // ... + * ``` + * + * ### Sharing tasks between [runBlocking] calls + * + * The event loop used by [runBlocking] is shared with the other [runBlocking] calls. + * This can lead to surprising and undesired behavior. + * + * ``` + * runBlocking { + * val job = launch { + * delay(50.milliseconds) + * println("Hello from the outer child coroutine") + * } + * runBlocking { + * println("Entered the inner runBlocking") + * delay(100.milliseconds) + * println("Leaving the inner runBlocking") + * } + * } + * ``` + * + * This outputs the following: + * + * ``` + * Entered the inner runBlocking + * Hello from the outer child coroutine + * Leaving the inner runBlocking + * ``` + * + * For example, the following code may fail with a stack overflow error: * - * If this blocked thread is interrupted (see `Thread.interrupt`), then the coroutine job is cancelled and - * this `runBlocking` invocation throws `InterruptedException`. + * ``` + * runBlocking { + * repeat(1000) { + * launch { + * try { + * runBlocking { + * // do nothing + * } + * } catch (e: Throwable) { + * println(e) + * } + * } + * } + * } + * ``` * - * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available - * for a newly created coroutine. + * The reason is that each new `runBlocking` attempts to run the task of the outer `runBlocking` coroutine inline, + * but those, in turn, start new `runBlocking` calls. * - * @param context the context of the coroutine. The default value is an event loop on the current thread. - * @param block the coroutine code. + * The specific behavior of work stealing may change in the future, but is unlikely to be fully fixed, + * given how widespread [runBlocking] is. */ @OptIn(ExperimentalContracts::class) @JvmName("runBlockingK") diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt index a3d0e0c4ad..2539ce562b 100644 --- a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt @@ -5,17 +5,21 @@ import kotlin.coroutines.* import kotlin.coroutines.jvm.internal.CoroutineStackFrame /** - * Creates a context for a new coroutine. It installs [Dispatchers.Default] when no other dispatcher or - * [ContinuationInterceptor] is specified and adds optional support for debugging facilities (when turned on) - * and copyable-thread-local facilities on JVM. - * See [DEBUG_PROPERTY_NAME] for description of debugging facilities on JVM. + * Creates a context for a new coroutine. * - * When [CopyableThreadContextElement] values are used, the logic for processing them is as follows: - * - If [this] or [context] has a copyable thread-local value whose key is absent in [context] or [this], - * it is [copied][CopyableThreadContextElement.copyForChild] to the new context. - * - If [this] has a copyable thread-local value whose key is present in [context], - * it is [merged][CopyableThreadContextElement.mergeForChild] with the one from [context]. - * - The other values are added to the new context as is, with [context] values taking precedence. + * See the documentation of this function's common-code implementation for a general overview. + * Here, the JVM-specific details are described. + * + * - If the [debug mode][DEBUG_PROPERTY_NAME] is enabled, + * [newCoroutineContext] assigns a unique identifier to every coroutine for tracking it. + * The ID is visible in [toString] of the coroutine context or its job and in the thread name, + * as well as in the dumps produced by the `kotlinx-coroutines-debug` module. + * - [CopyableThreadContextElement] values are combined. + * If both the parent and [context] have the same [CopyableThreadContextElement] + * (when their [key][CoroutineContext.Key] is equal), + * the values are [merged][CopyableThreadContextElement.mergeForChild]. + * If only the parent or only the [context] has the [CopyableThreadContextElement], + * the value is [copied][CopyableThreadContextElement.copyForChild]. */ @ExperimentalCoroutinesApi public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {