Skip to content
409 changes: 359 additions & 50 deletions kotlinx-coroutines-core/common/src/Builders.common.kt

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions kotlinx-coroutines-core/common/src/CoroutineContext.common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1,208 changes: 1,074 additions & 134 deletions kotlinx-coroutines-core/common/src/CoroutineScope.kt

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions kotlinx-coroutines-core/common/src/NonCancellable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
public object NonCancellable : AbstractCoroutineContextElement(Job), Job {
Expand Down
75 changes: 65 additions & 10 deletions kotlinx-coroutines-core/common/src/Supervisor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>): List<Deferred<ByteArray>> =
* 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 <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
contract {
Expand Down
Loading