Skip to content

Commit 16a4eef

Browse files
committed
Document withContext and some more pitfalls
1 parent 9c180e8 commit 16a4eef

File tree

4 files changed

+328
-17
lines changed

4 files changed

+328
-17
lines changed

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

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -320,28 +320,131 @@ private class LazyDeferredCoroutine<T>(
320320
// --------------- withContext ---------------
321321

322322
/**
323-
* Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns
323+
* Calls the specified suspending [block] with an updated coroutine context, suspends until it completes, and returns
324324
* the result.
325325
*
326-
* The resulting context for the [block] is derived by merging the current [coroutineContext] with the
327-
* specified [context] using `coroutineContext + context` (see [CoroutineContext.plus]).
328-
* This suspending function is cancellable. It immediately checks for cancellation of
329-
* the resulting context and throws [CancellationException] if it is not [active][CoroutineContext.isActive].
326+
* [context] specifies the additional context elements for the coroutine to combine with
327+
* the elements already present in the [CoroutineScope.coroutineContext].
328+
* It is incorrect to pass a [Job] element there, as this breaks structured concurrency,
329+
* unless it is [NonCancellable].
330+
*
331+
* If the resulting [CoroutineScope.coroutineContext] is cancelled before the [block] starts running,
332+
* [block] will immediately finish with a [CancellationException],
333+
* possibly without even being scheduled for execution.
334+
*
335+
* ## Structured Concurrency
336+
*
337+
* The behavior of [withContext] is similar to [coroutineScope], as it, too, creates a new *scoped child coroutine*.
338+
* Refer to the documentation of that function for details.
339+
*
340+
* The difference is that [withContext] does not simply call the [block] in a new coroutine
341+
* but updates the [currentCoroutineContext] used for running it.
342+
*
343+
* The context of the new scope is created like this:
344+
* - First, [currentCoroutineContext] is combined with the [context] argument.
345+
* In most cases, this means that elements from [context] simply override
346+
* the elements in the [currentCoroutineContext],
347+
* but if they are `CopyableThreadContextElement`s, they are copied and combined as needed.
348+
* - Then, the [Job] in the [currentCoroutineContext], if any, is used as the *parent* of the new scope,
349+
* unless overridden.
350+
* Overriding the [Job] is forbidden with the notable exception of [NonCancellable];
351+
* see a separate subsection below for details.
352+
* The new scope's [Job] is added to the resulting context.
353+
*
354+
* The context of the new scope is obtained by combining the [currentCoroutineContext] with a new [Job]
355+
* whose parent is the [Job] of the caller [currentCoroutineContext] (if any).
356+
* The [Job] of the new scope is not a normal child of the caller coroutine but a lexically scoped one,
357+
* meaning that the failure of the [Job] will not affect the parent [Job].
358+
* Instead, the exception leading to the failure will be rethrown to the caller of this function.
359+
*
360+
* ### Overriding the parent job
361+
*
362+
* #### [NonCancellable]
363+
*
364+
* Passing [NonCancellable] in the [context] argument is a special case that allows
365+
* the [block] to run even if the parent coroutine is cancelled.
366+
*
367+
* This is useful in particular for performing cleanup operations
368+
* if the cleanup procedure is itself a `suspend` function.
369+
*
370+
* Example:
371+
*
372+
* ```
373+
* class Connection {
374+
* suspend fun terminate()
375+
* }
376+
*
377+
* val connection = Connection()
378+
* try {
379+
* // some cancellable operations...
380+
* } finally {
381+
* withContext(NonCancellable) {
382+
* // this block will run even if the parent coroutine is cancelled
383+
* connection.terminate()
384+
* }
385+
* }
386+
* ```
387+
*
388+
* Beware that combining [NonCancellable] with context elements that change the dispatcher
389+
* will make this cleanup code incorrect. See the [NonCancellable] documentation for details.
390+
*
391+
* #### Other [Job] elements
392+
*
393+
* Passing a [Job] in the [context] argument breaks structured concurrency and is not a supported pattern.
394+
* It does not throw an exception only for backward compatibility reasons, as a lot of code was written this way.
395+
* Always structure your coroutines such that the lifecycle of the child coroutine is
396+
* contained in the lifecycle of the [CoroutineScope] it is launched in.
397+
*
398+
* To help with migrating to structured concurrency, the specific behaviour of passing a [Job] in the [context] argument
399+
* is described here.
400+
* **Do not rely on this behaviour in new code.**
401+
*
402+
* If [context] contains a [Job] element, it will be the *parent* of the new coroutine,
403+
* and the lifecycle of the new coroutine will not be tied to the [CoroutineScope] at all.
330404
*
331-
* Calls to [withContext] whose [context] argument provides a [CoroutineDispatcher] that is
332-
* different from the current one, by necessity, perform additional dispatches: the [block]
333-
* can not be executed immediately and needs to be dispatched for execution on
334-
* the passed [CoroutineDispatcher], and then when the [block] completes, the execution
335-
* has to shift back to the original dispatcher.
405+
* In specific terms:
336406
*
337-
* Note that the result of `withContext` invocation is dispatched into the original context in a cancellable way
407+
* - If the [Job] passed to the [context] is cancelled, the new coroutine will be cancelled.
408+
* - However, because [withContext] creates a scoped child coroutine, the failure of [block]
409+
* will not cancel the parent [Job].
410+
* - If the [currentCoroutineContext] is cancelled, the new coroutine will not be affected.
411+
* - In particular, if [withContext] avoided a dispatch (see "Dispatching behavior" below)
412+
* and its block finished without an exception, [withContext] itself will not throw a [CancellationException].
413+
* This means that the block execution will continue even if the parent coroutine was cancelled.
414+
*
415+
* ## Dispatching behavior
416+
*
417+
* If [context] provides a [ContinuationInterceptor] other than the one used by the caller,
418+
* the [block] can not simply be called inline with the updated context and has to go through a dispatch,
419+
* that is, get scheduled on the new [ContinuationInterceptor].
420+
* It is up to the [ContinuationInterceptor] to actually run the [block],
421+
* and it may take arbitrarily long for that to happen.
422+
* After the [block] has completed, the computation has to be dispatched back to the original
423+
* [ContinuationInterceptor].
424+
*
425+
* If the resulting context in which [block] should run has the same [ContinuationInterceptor]
426+
* as the caller, no dispatch is performed.
427+
* In that case, no dispatch happens on exiting the [block] either, unless child coroutines have to be awaited.
428+
*
429+
* Note that the result of [withContext] invocation is dispatched into the original context in a cancellable way
338430
* with a **prompt cancellation guarantee**, which means that if the original [coroutineContext]
339-
* in which `withContext` was invoked is cancelled by the time its dispatcher starts to execute the code,
340-
* it discards the result of `withContext` and throws [CancellationException].
431+
* in which [withContext] was invoked is cancelled by the time its dispatcher starts to execute the code,
432+
* it discards the result of [withContext] and throws a [CancellationException].
433+
*
434+
* Note that if the dispatch from [withContext] back to the original context does not need to happen
435+
* (because of having the same dispatcher and not having to wait for the children)
436+
* *and* the [context] passed to [withContext] contains [NonCancellable],
437+
* then cancellation of the caller will not prevent a value from being successfully returned.
438+
*
439+
* ## Pitfalls
440+
*
441+
* ### Returning closeable resources
442+
*
443+
* Values returned from [withContext] will typically be lost if the caller is cancelled.
444+
* The exception is the `withContext(NonCancellable)` pattern.
341445
*
342-
* The cancellation behaviour described above is enabled if and only if the dispatcher is being changed.
343-
* For example, when using `withContext(NonCancellable) { ... }` there is no change in dispatcher and
344-
* this call will not be cancelled neither on entry to the block inside `withContext` nor on exit from it.
446+
* See the corresponding section in the [coroutineScope] documentation for details,
447+
* as well as the [NonCancellable] documentation.
345448
*/
346449
public suspend fun <T> withContext(
347450
context: CoroutineContext,

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,61 @@ public object GlobalScope : CoroutineScope {
797797
*
798798
* There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled
799799
* while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details.
800+
*
801+
* ## Pitfall: returning closeable resources from a scoped coroutine
802+
*
803+
* [R] must be a value that can safely be dropped. For example, this code is incorrect:
804+
*
805+
* ```
806+
* // DO NOT DO THIS
807+
* val closeableResource = coroutineScope {
808+
* // calculate the closeable resource somehow
809+
* obtainResource()
810+
* }
811+
* closeableResource.use { resource ->
812+
* // use the resource
813+
* }
814+
* ```
815+
*
816+
* The problem is that, if the caller gets cancelled before [coroutineScope] completes,
817+
* then even if the calculation of the closeable resource does not suspend at all,
818+
* [coroutineScope] will throw [CancellationException] instead of returning any value.
819+
*
820+
* This pitfall applies to all [coroutineScope]-like functions, like [withContext], [withTimeout], or [supervisorScope].
821+
* For this discussion, we call them collectively `myLexicalScope`.
822+
*
823+
* If it is necessary to process a value returned from a lexical coroutine scope, the following pattern should be used:
824+
*
825+
* ```
826+
* var resource: MyResource? = null
827+
* try {
828+
* // note: do not simply return the resource here!
829+
* myLexicalScope {
830+
* resource = obtainResource()
831+
* }
832+
* // the resource is available here
833+
* } finally {
834+
* resource?.close()
835+
* }
836+
* ```
837+
*
838+
* If cancellation during the acquisition of the resource is also undesired, the following pattern can be used
839+
* ([coroutineScope] is not needed here, as [withContext] defines its own [CoroutineScope], but :
840+
*
841+
* ```
842+
* withContext(NonCancellable) {
843+
* myLexicalScope {
844+
* obtainResource()
845+
* }
846+
* }.use { resource ->
847+
* }
848+
* ```
849+
*
850+
* Be aware, however, that like any [NonCancellable] usage, this creates the risk of accessing values past the point
851+
* where they are valid.
852+
* For example, if the caller coroutine scope is tied to the lifecycle of a UI element, with cancellation meaning
853+
* that the UI element was already disposed of, accessing it during the acquisition of a resource or
854+
* before the first suspension point in [use] is not allowed and may lead to crashes.
800855
*/
801856
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
802857
contract {

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

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,150 @@ import kotlin.coroutines.*
2020
* if you write `launch(NonCancellable) { ... }` then not only the newly launched job will not be cancelled
2121
* when the parent is cancelled, the whole parent-child relation between parent and child is severed.
2222
* The parent will not wait for the child's completion, nor will be cancelled when the child crashed.
23+
*
24+
* ## Pitfalls
25+
*
26+
* ### Overriding the exception with a [CancellationException] in a finalizer
27+
*
28+
* #### Combining [NonCancellable] with a [ContinuationInterceptor]
29+
*
30+
* The typical usage of [NonCancellable] is to ensure that cleanup code is executed even if the parent job is cancelled.
31+
* Example:
32+
*
33+
* ```
34+
* try {
35+
* // some code using a resource
36+
* } finally {
37+
* withContext(NonCancellable) {
38+
* // cleanup code that should not be cancelled
39+
* }
40+
* }
41+
* ```
42+
*
43+
* However, it is easy to get this pattern wrong if the cleanup code needs to run on some specific dispatcher:
44+
*
45+
* ```
46+
* // DO NOT DO THIS
47+
* withContext(Dispatchers.Main) {
48+
* try {
49+
* // some code using a resource
50+
* } finally {
51+
* // THIS IS INCORRECT
52+
* withContext(NonCancellable + Dispatchers.Default) {
53+
* // cleanup code that should not be cancelled
54+
* } // this line may throw a `CancellationException`!
55+
* }
56+
* }
57+
* ```
58+
*
59+
* In this case, if the parent job is cancelled, [withContext] will throw a [CancellationException] as soon
60+
* as it tries to switch back from the [Dispatchers.Default] dispatcher back to the original one.
61+
* The reason for this is that [withContext] obeys the **prompt cancellation** principle,
62+
* which means that dispatching back from it to the original context will fail with a [CancellationException]
63+
* even if the block passed to [withContext] finished successfully,
64+
* overriding the original exception thrown by the `try` block, if any.
65+
*
66+
* To avoid this, you should use [NonCancellable] as the only element in the context of the `withContext` call,
67+
* and then inside the block, you can switch to any dispatcher you need:
68+
*
69+
* ```
70+
* withContext(Dispatchers.Main) {
71+
* try {
72+
* // some code using a resource
73+
* } finally {
74+
* withContext(NonCancellable) {
75+
* withContext(Dispatchers.Default) {
76+
* // cleanup code that should not be cancelled
77+
* }
78+
* }
79+
* }
80+
* }
81+
* ```
82+
*
83+
* #### Launching child coroutines
84+
*
85+
* Child coroutines should not be started in `withContext(NonCancellable)` blocks in resource cleanup handlers directly.
86+
*
87+
* ```
88+
* // DO NOT DO THIS
89+
* withContext(Dispatchers.Main) {
90+
* try {
91+
* // some code using a resource
92+
* } finally {
93+
* // THIS IS INCORRECT
94+
* withContext(NonCancellable) {
95+
* // cleanup code that should not be cancelled
96+
* launch { delay(100.milliseconds) }
97+
* } // this line may throw a `CancellationException`!
98+
* }
99+
* }
100+
* ```
101+
*
102+
* Similarly to the case of specifying a dispatcher alongside [NonCancellable] in a [withContext] argument,
103+
* having to wait for child coroutines can lead to a dispatch at the end of the [withContext] call,
104+
* which will lead to it throwing a [CancellationException] due to the prompt cancellation guarantee.
105+
*
106+
* The solution to this is also similar:
107+
*
108+
* ```
109+
* withContext(Dispatchers.Main) {
110+
* try {
111+
* // some code using a resource
112+
* } finally {
113+
* withContext(NonCancellable) {
114+
* // note: `coroutineScope` here is required
115+
* // to prevent a sporadic CancellationException
116+
* coroutineScope {
117+
* // cleanup code that should not be cancelled
118+
* launch { delay(100.milliseconds) }
119+
* }
120+
* }
121+
* }
122+
* }
123+
* ```
124+
*
125+
* Because now [coroutineScope] and not [withContext] has to wait for the children, there is once again no dispatch
126+
* between the last line of the [withContext] block and getting back to the caller.
127+
*
128+
* ### Not reacting to cancellations right outside the [withContext]
129+
*
130+
* Just like combining [NonCancellable] with other elements is incorrect because cancellation may override
131+
* the original exception, the opposite can also be incorrect, depending on the context:
132+
*
133+
* ```
134+
* // DO NOT DO THIS
135+
* withContext(Dispatchers.Main) {
136+
* withContext(NonCancellable) {
137+
* withContext(Dispatchers.Default) {
138+
* // do something
139+
* }
140+
* } // will not react to the caller's cancellation!
141+
* // BUG HERE
142+
* updateUi() // may be invoked when the caller is already cancelled
143+
* }
144+
* ```
145+
*
146+
* Here, the following may happen:
147+
* 1. The `do something` block gets entered, and the main thread gets released and is free to perform other tasks.
148+
* 2. Some other task updates the UI and cancels this coroutine, which is no longer needed.
149+
* 3. `do something` finishes, and the computation is dispatched back to the main thread.
150+
* 4. `updateUi()` is called, even though the coroutine was already cancelled and the UI is no longer in a valid state
151+
* for this update operation, potentially leading to a crash.
152+
*
153+
* [ensureActive] can be used to manually ensure that cancelled code no longer runs:
154+
*
155+
* ```
156+
* withContext(Dispatchers.Main) {
157+
* withContext(NonCancellable) {
158+
* withContext(Dispatchers.Default) {
159+
* // do something
160+
* }
161+
* }
162+
* ensureActive() // check if we are still allowed to run the code
163+
* updateUi()
164+
* }
165+
* ```
166+
*
23167
*/
24168
@OptIn(InternalForInheritanceCoroutinesApi::class)
25169
public object NonCancellable : AbstractCoroutineContextElement(Job), Job {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent)
7777
* There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled
7878
* while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details.
7979
*
80-
* **Pitfall**: [supervisorScope] does not install a [CoroutineExceptionHandler] in the new scope.
80+
* ## Pitfalls
81+
*
82+
* ### Uncaught exceptions in child coroutines
83+
*
84+
* [supervisorScope] does not install a [CoroutineExceptionHandler] in the new scope.
8185
* This means that if a child coroutine started with [launch] fails, its exception will be unhandled,
8286
* possibly crashing the program. Use the following pattern to avoid this:
8387
*
@@ -92,6 +96,11 @@ public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent)
9296
* ```
9397
*
9498
* Alternatively, the [CoroutineExceptionHandler] can be supplied to the newly launched coroutines themselves.
99+
*
100+
* ### Returning closeable resources
101+
*
102+
* Values returned from [supervisorScope] will be lost if the caller is cancelled.
103+
* See the corresponding section in the [coroutineScope] documentation for details.
95104
*/
96105
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
97106
contract {

0 commit comments

Comments
 (0)