Skip to content

Document structured concurrency #4433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,203 changes: 1,069 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)
@Suppress("DeprecatedCallableAddReplaceWith")
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
103 changes: 95 additions & 8 deletions kotlinx-coroutines-core/common/src/Timeout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,11 @@ public suspend fun <T> 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
Expand All @@ -64,6 +60,97 @@ public suspend fun <T> 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 <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
contract {
Expand Down
Loading