Skip to content

Commit 6515270

Browse files
committed
Document supervisorScope
1 parent d00844d commit 6515270

File tree

2 files changed

+59
-10
lines changed

2 files changed

+59
-10
lines changed

kotlinx-coroutines-core/common/src/CoroutineScope.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,9 @@ public object GlobalScope : CoroutineScope {
791791
* Note that this happens on any child coroutine's failure even if [block] finishes successfully.
792792
* - [coroutineScope] will only finish when all the coroutines launched in it finish.
793793
* If all of them complete without failing, the [coroutineScope] returns the result of the [block] to the caller.
794+
*
795+
* There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled
796+
* while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details.
794797
*/
795798
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
796799
contract {

kotlinx-coroutines-core/common/src/Supervisor.kt

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,65 @@ public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobIm
3333
public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent)
3434

3535
/**
36-
* Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend [block] with this scope.
37-
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, using the
38-
* [Job] from that context as the parent for the new [SupervisorJob].
39-
* This function returns as soon as the given block and all its child coroutines are completed.
36+
* Runs the given [block] in-place in a new [CoroutineScope] with a [SupervisorJob]
37+
* based on the caller coroutine context, returning its result.
4038
*
41-
* Unlike [coroutineScope], a failure of a child does not cause this scope to fail and does not affect its other children,
42-
* so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for additional details.
39+
* The lifecycle of the new [SupervisorJob] begins with starting the [block] and completes when both the [block] and
40+
* all the coroutines launched in the scope complete.
4341
*
44-
* If an exception happened in [block], then the supervisor job is failed and all its children are cancelled.
45-
* If the current coroutine was cancelled, then both the supervisor job itself and all its children are cancelled.
42+
* The context of the new scope is obtained by combining the [currentCoroutineContext] with a new [SupervisorJob]
43+
* whose parent is the [Job] of the caller [currentCoroutineContext] (if any).
44+
* The [SupervisorJob] of the new scope is not a normal child of the caller coroutine but a lexically scoped one,
45+
* meaning that the failure of the [SupervisorJob] will not affect the parent [Job].
46+
* Instead, the exception leading to the failure will be rethrown to the caller of this function.
4647
*
47-
* The method may throw a [CancellationException] if the current job was cancelled externally,
48-
* or rethrow an exception thrown by the given [block].
48+
* If a child coroutine launched in the new scope fails, it will not affect the other children of the scope.
49+
* However, if the [block] finishes with an exception, it will cancel the scope and all its children.
50+
* See [coroutineScope] for a similar function that treats every child coroutine as crucial for obtaining the result
51+
* and cancels the whole computation if one of them fails.
52+
*
53+
* Together, this makes [supervisorScope] a good choice for launching multiple coroutines where some failures
54+
* are acceptable and should not affect the others.
55+
*
56+
* ```
57+
* // cancelling the caller's coroutine will cancel the new scope and all its children
58+
* suspend fun tryDownloadFiles(urls: List<String>): List<Deferred<ByteArray>> =
59+
* supervisorScope {
60+
* urls.map { url ->
61+
* async {
62+
* // if one of the downloads fails, the others will continue
63+
* donwloadFileContent(url)
64+
* }
65+
* }
66+
* } // every download will fail or complete by the time this function returns
67+
* ```
68+
*
69+
* Rephrasing this in more practical terms, the specific list of structured concurrency interactions is as follows:
70+
* - Cancelling the caller's [currentCoroutineContext] leads to cancellation of the new [CoroutineScope]
71+
* (corresponding to the code running in the [block]), which in turn cancels all the coroutines launched in it.
72+
* - If the [block] fails with an exception, the exception is rethrown to the caller,
73+
* without directly affecting the caller's [Job].
74+
* - [supervisorScope] will only finish when all the coroutines launched in it finish.
75+
* After that, the [supervisorScope] returns (or rethrows) the result of the [block] to the caller.
76+
*
77+
* There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled
78+
* while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details.
79+
*
80+
* **Pitfall**: [supervisorScope] does not install a [CoroutineExceptionHandler] in the new scope.
81+
* This means that if a child coroutine started with [launch] fails, its exception will be unhandled,
82+
* possibly crashing the program. Use the following pattern to avoid this:
83+
*
84+
* ```
85+
* withContext(CoroutineExceptionHandler { _, exception ->
86+
* // handle the exceptions as needed
87+
* }) {
88+
* supervisorScope {
89+
* // launch child coroutines here
90+
* }
91+
* }
92+
* ```
93+
*
94+
* Alternatively, the [CoroutineExceptionHandler] can be supplied to the newly launched coroutines themselves.
4995
*/
5096
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
5197
contract {

0 commit comments

Comments
 (0)