@@ -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 */
346449public suspend fun <T > withContext (
347450 context : CoroutineContext ,
0 commit comments