Skip to content

Commit fc6e842

Browse files
committed
Document CoroutineScope.launch
1 parent 6515270 commit fc6e842

File tree

2 files changed

+163
-17
lines changed

2 files changed

+163
-17
lines changed

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

Lines changed: 158 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,168 @@ import kotlin.jvm.*
1717
// --------------- launch ---------------
1818

1919
/**
20-
* Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job].
21-
* The coroutine is cancelled when the resulting job is [cancelled][Job.cancel].
20+
* Launches a new *child coroutine* of [CoroutineScope] without blocking the current thread
21+
* and returns a reference to the coroutine as a [Job].
2222
*
23-
* The coroutine context is inherited from a [CoroutineScope]. Additional context elements can be specified with [context] argument.
24-
* If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.
25-
* The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden
26-
* with a corresponding [context] element.
23+
* [block] is the computation of the new coroutine that will run concurrently.
24+
* The coroutine is considered active until the block and all the child coroutines created in it finish.
2725
*
28-
* By default, the coroutine is immediately scheduled for execution.
29-
* Other start options can be specified via `start` parameter. See [CoroutineStart] for details.
30-
* An optional [start] parameter can be set to [CoroutineStart.LAZY] to start coroutine _lazily_. In this case,
31-
* the coroutine [Job] is created in _new_ state. It can be explicitly started with [start][Job.start] function
32-
* and will be started implicitly on the first invocation of [join][Job.join].
26+
* [context] specifies the additional context elements for the coroutine to combine with
27+
* the elements already present in the [CoroutineScope.coroutineContext].
28+
* It is incorrect to pass a [Job] element there, as this breaks structured concurrency.
29+
*
30+
* By default, the coroutine is scheduled for execution on its [ContinuationInterceptor].
31+
* There is no guarantee that it will start immediately: this is decided by the [ContinuationInterceptor].
32+
* It is possible that the new coroutine will be cancelled before starting, in which case its code will not be executed.
33+
* The [start] parameter can be used to adjust this behavior. See [CoroutineStart] for details.
34+
*
35+
* ## Structured Concurrency
36+
*
37+
* [launch] creates a *child coroutine* of `this` [CoroutineScope].
38+
*
39+
* The context of the new coroutine is created like this:
40+
* - First, the context of the [CoroutineScope] is combined with the [context] argument
41+
* using the [newCoroutineContext] function.
42+
* In most cases, this means that elements from [context] simply override
43+
* the elements in the [CoroutineScope.coroutineContext].
44+
* If no [ContinuationInterceptor] is present in the resulting context,
45+
* then [Dispatchers.Default] is added there.
46+
* - Then, the [Job] in the [CoroutineScope.coroutineContext] is used as the *parent* of the new coroutine,
47+
* unless overridden.
48+
* Overriding the [Job] is forbidden; see a separate subsection below for details.
49+
* The new coroutine's [Job] is added to the resulting context.
50+
*
51+
* The resulting coroutine context is the [coroutineContext] of the [CoroutineScope]
52+
* passed to the [block] as its receiver.
53+
*
54+
* The new coroutine is considered [active][isActive] until the [block] and all its child coroutines finish.
55+
* If the [block] throws a [CancellationException], the coroutine is considered cancelled,
56+
* and if it throws any other exception, the coroutine is considered failed.
57+
*
58+
* The details of structured concurrency are described in the [CoroutineScope] interface documentation.
59+
* Here is a restatement of some main points as they relate to `launch`:
60+
*
61+
* - The lifecycle of the parent [CoroutineScope] can not end until this coroutine
62+
* (as well as all its children) completes.
63+
* - If the parent [CoroutineScope] is cancelled, this coroutine is cancelled as well.
64+
* - If this coroutine fails with a non-[CancellationException] exception
65+
* and the parent [CoroutineScope] has a non-supervisor [Job] in its context,
66+
* the parent [Job] is cancelled with this exception.
67+
* - If this coroutine fails with an exception and the parent [CoroutineScope] has a supervisor [Job] or no job at all
68+
* (as is the case with [GlobalScope] or malformed scopes),
69+
* the exception is considered uncaught and is propagated as the [CoroutineExceptionHandler] documentation describes.
70+
* - The lifecycle of the [CoroutineScope] passed as the receiver to the [block]
71+
* will not end until the [block] completes (or gets cancelled before ever having a chance to run).
72+
* - If the [block] throws a [CancellationException], the coroutine is considered cancelled,
73+
* cancelling all its children in turn, but the parent does not get notified.
74+
*
75+
* ### Overriding the parent job
76+
*
77+
* Passing a [Job] in the [context] argument breaks structured concurrency and is not a supported pattern.
78+
* It does not throw an exception only for backward compatibility reasons, as a lot of code was written this way.
79+
* Always structure your coroutines such that the lifecycle of the child coroutine is
80+
* contained in the lifecycle of the [CoroutineScope] it is launched in.
81+
*
82+
* To help with migrating to structured concurrency, the specific behaviour of passing a [Job] in the [context] argument
83+
* is described here.
84+
* **Do not rely on this behaviour in new code.**
85+
*
86+
* If [context] contains a [Job] element, it will be the *parent* of the new coroutine,
87+
* and the lifecycle of the new coroutine will not be tied to the [CoroutineScope] at all.
88+
*
89+
* In specific terms:
90+
*
91+
* - If the [CoroutineScope] is cancelled, the new coroutine will not be affected.
92+
* - If the new coroutine fails with an exception, it will not cancel the [CoroutineScope].
93+
* Instead, the exception will be propagated to the [Job] passed in the [context] argument.
94+
* If that [Job] is a [SupervisorJob], the exception will be unhandled,
95+
* and will be propagated as the [CoroutineExceptionHandler] documentation describes.
96+
* If that [Job] is not a [SupervisorJob], it will be cancelled with the exception thrown by [launch].
97+
* - If the [CoroutineScope] is lexically scoped (for example, created by [coroutineScope] or [withContext]),
98+
* the function defining the scope will not wait for the new coroutine to finish.
99+
*
100+
* ## Communicating with the coroutine
101+
*
102+
* [Job.cancel] can be used to cancel the coroutine, and [Job.join] can be used to block until its completion
103+
* without blocking the current thread.
104+
* Note that [Job.join] succeeds even if the coroutine was cancelled or failed with an exception.
105+
* [Job.cancelAndJoin] is a convenience function that combines cancellation and joining.
106+
*
107+
* If the coroutine was started with [start] set to [CoroutineStart.LAZY], the coroutine will not be scheduled
108+
* to run on its [ContinuationInterceptor] immediately.
109+
* [Job.start] can be used to start the coroutine explicitly,
110+
* and awaiting its completion using [Job.join] also causes the coroutine to start executing.
111+
*
112+
* A coroutine created with [launch] does not return a result, and if it fails with an exception,
113+
* there is no reliable way to learn about that exception in general.
114+
* [async] is a better choice if the result of the coroutine needs to be accessed from another coroutine.
115+
*
116+
* ## Pitfalls
117+
*
118+
* ### [CancellationException] silently stopping computations
119+
*
120+
* ```
121+
* val deferred = GlobalScope.async {
122+
* awaitCancellation()
123+
* }
124+
* deferred.cancel()
125+
* coroutineScope {
126+
* val job = launch {
127+
* val result = deferred.await()
128+
* println("Got $result")
129+
* }
130+
* job.join()
131+
* println("Am I still not cancelled? $isActive")
132+
* }
133+
* ```
134+
*
135+
* will output
136+
*
137+
* ```
138+
* Am I still not cancelled? true
139+
* ```
140+
*
141+
* This may be surprising, because the `launch`ed coroutine failed with an exception,
142+
* but the parent still was not cancelled.
143+
*
144+
* The reason for this is that any [CancellationException] thrown in the coroutine is treated as a signal to cancel
145+
* the coroutine, but not the parent.
146+
* In this scenario, this is unlikely to be the desired behaviour:
147+
* this was a failure and not a cancellation and should be propagated to the parent.
148+
*
149+
* This is a legacy behavior that cannot be changed in a backward-compatible way.
150+
* Use [ensureActive] and [isActive] to distinguish between cancellation and failure:
151+
*
152+
* ```
153+
* launch {
154+
* try {
155+
* val result = deferred.await()
156+
* } catch (e: CancellationException) {
157+
* if (isActive) {
158+
* // we were not cancelled, this is a failure
159+
* println("`result` was cancelled")
160+
* throw IllegalStateException("$result was cancelled", e)
161+
* } else {
162+
* println("I was cancelled")
163+
* // throw again to finish the coroutine
164+
* ensureActive()
165+
* }
166+
* }
167+
* }
168+
* ```
33169
*
34-
* Uncaught exceptions in this coroutine cancel the parent job in the context by default
35-
* (unless [CoroutineExceptionHandler] is explicitly specified), which means that when `launch` is used with
36-
* the context of another coroutine, then any uncaught exception leads to the cancellation of the parent coroutine.
170+
* In simpler scenarios, this form can be used:
37171
*
38-
* See [newCoroutineContext] for a description of debugging facilities that are available for a newly created coroutine.
172+
* ```
173+
* launch {
174+
* try {
175+
* // operation that may throw its own CancellationException
176+
* } catch (e: CancellationException) {
177+
* ensureActive()
178+
* throw IllegalStateException(e)
179+
* }
180+
* }
181+
* ```
39182
*
40183
* @param context additional to [CoroutineScope.coroutineContext] context of the coroutine.
41184
* @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ import kotlin.coroutines.intrinsics.*
327327
* job.join() // finishes normally
328328
* ```
329329
*
330-
* If a coroutine fails with an exception,
330+
* If a coroutine fails with a non-[CancellationException] exception,
331331
* is not a coroutine created with lexically scoped coroutine builders like [coroutineScope] or [withContext],
332332
* *and* its parent is a normal [Job] (not a [SupervisorJob]),
333333
* the parent fails with that exception, too (and the same logic applies recursively to the parent of the parent, etc.):
@@ -374,11 +374,14 @@ import kotlin.coroutines.intrinsics.*
374374
* the first one to fail will be propagated, and the rest will be attached to it as
375375
* [suppressed exceptions][Throwable.suppressedExceptions].
376376
*
377-
* If a coroutine fails with an exception and cannot cancel its parent
377+
* If a coroutine fails with a non-[CancellationException] exception and cannot cancel its parent
378378
* (because its parent is a [SupervisorJob] or there is none at all),
379379
* the failure is reported through other channels.
380380
* See [CoroutineExceptionHandler] for details.
381381
*
382+
* Failing with a [CancellationException] only cancels the coroutine itself and its children.
383+
* It does not affect the parent or any other coroutines and is not considered a failure.
384+
*
382385
* ### How-to: stop failures of child coroutines from cancelling other coroutines
383386
*
384387
* If not affecting the [CoroutineScope] on a failure in a child coroutine is the desired behaviour,

0 commit comments

Comments
 (0)