From 3ec4dba1f4959d3ff8b1ecb8ac1004c1ba09df69 Mon Sep 17 00:00:00 2001 From: Jared Sinclair Date: Fri, 5 Dec 2025 08:19:35 -0500 Subject: [PATCH 1/3] Improve Task documentation. Clarifies that Task instances do not inherit cancellation from an enclosing Task within which they are created. Clarifies that the term "child task" does not refer to such tasks. Clarifies some language which had previously made it seem like non-detached Tasks would inherit cancellation (they do not). --- stdlib/public/Concurrency/Task+init.swift.gyb | 13 +++-- stdlib/public/Concurrency/Task.swift | 55 ++++++++++++++++--- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/stdlib/public/Concurrency/Task+init.swift.gyb b/stdlib/public/Concurrency/Task+init.swift.gyb index 36a2e597e0be3..65c37db6a1554 100644 --- a/stdlib/public/Concurrency/Task+init.swift.gyb +++ b/stdlib/public/Concurrency/Task+init.swift.gyb @@ -253,11 +253,14 @@ extension Task where Failure == ${FAILURE_TYPE} { % end /// % if IS_DETACHED: - /// Don't use a detached unstructured task if it's possible - /// to model the operation using structured concurrency features like child tasks. - /// Child tasks inherit the parent task's priority and task-local storage, - /// and canceling a parent task automatically cancels all of its child tasks. - /// You need to handle these considerations manually with a detached task. + /// In most cases, an "attached" unstructured task (created via `Task.init`) + /// is a more appropriate choice than a detached unstructured task. Attached + /// tasks inherit the priority and task-local storage from the enclosing task + /// during which they are created. You need to handle these considerations + /// manually with a detached task. + /// + /// A detached `Task` has the same cancellation mechanism as any other `Task`; + /// it can only become cancelled via an explicit `cancel()` method call. % end /// /// You need to keep a reference to the task diff --git a/stdlib/public/Concurrency/Task.swift b/stdlib/public/Concurrency/Task.swift index 3ad1294e79ee5..c4526cdfd374c 100644 --- a/stdlib/public/Concurrency/Task.swift +++ b/stdlib/public/Concurrency/Task.swift @@ -30,9 +30,9 @@ import Swift /// you give up the ability /// to wait for that task's result or cancel the task. /// -/// To support operations on the current task, -/// which can be either a detached task or child task, -/// `Task` also exposes class methods like `yield()`. +/// To support operations on the current task, `Task` also exposes class methods +/// like `yield()`. The current task can be either an unstructured `Task`, a +/// detached unstructured `Task`, or a child task (an `async let` or a task group). /// Because these methods are asynchronous, /// they're always invoked as part of an existing task. /// @@ -77,6 +77,38 @@ import Swift /// This reflects the fact that a task can be canceled for many reasons, /// and additional reasons can accrue during the cancellation process. /// +/// A `Task` becomes cancelled when its `cancel()` method is called on it. There +/// is no other mechanism by which a `Task` can become cancelled. This is true +/// even of a `Task` instance which is created inside the operation of another +/// `Task`. You can manually propagate cancellation from an enclosing `Task` to +/// another task created during its operation via `withTaskCancellationHandler(operation:onCancel:isolation:)`: +/// +/// ```swift +/// let outer = Task { +/// let inner = Task { +/// // The next statement will not throw an error unless cancellation of +/// // the `outer` task is manually propagated to the `inner` task via a +/// // cancellation handler: +/// try Task.checkCancellation() +/// return try await work() +/// } +/// return try await withTaskCancellationHandler { +/// try await inner.value +/// } onCancel: { +/// inner.cancel() +/// } +/// } +/// +/// // When `outer` becomes cancelled, the `onCancel` handler above will run, +/// // causing `inner` to become cancelled, too. +/// outer.cancel() +/// ``` +/// +/// Any instance of `Task` that you initialize for yourself is a top-level, +/// _unstructured_ task. For more information about the implications of +/// unstructured tasks, read the ["Unstructured Concurrency"](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Unstructured-Concurrency) +/// portion of the Swift Programming Language book. +/// /// ### Task closure lifetime /// Tasks are initialized by passing a closure containing the code that will be executed by a given task. /// @@ -200,9 +232,16 @@ extension Task { /// /// - It flags the task as canceled. /// - It causes any active cancellation handlers on the task to run, once. - /// - It cancels any child tasks and task groups of the task, including - /// those created in the future. If those tasks have cancellation handlers, - /// they also are triggered. + /// - It cancels any child tasks (lowercase-t tasks, like `async let` and + /// other forms of structured concurrency, not capital-t `Task` instances) + /// and task groups of the task, including those created in the future. If + /// those tasks have cancellation handlers, they also are triggered. + /// + /// Cancelling a `Task` does _not_ cancel other instances of `Task` that were + /// or will be created during its operation. Those other instances will not + /// become cancelled until and unless they are explicitly cancelled via + /// their `cancel()` methods being called on them. Read the "Task Cancellation" + /// section of the `Task` documentation for more information. /// /// Task cancellation is cooperative and idempotent. /// @@ -671,8 +710,8 @@ extension Task where Success == Never, Failure == Never { /// and save it for long-term use. /// To query the current task without saving a reference to it, /// use properties like `currentPriority`. -/// If you need to store a reference to a task, -/// create an unstructured task using `Task.detached(priority:operation:)` instead. +/// If you need to store a reference to a task, create an unstructured task using +/// `Task.init(name:priority:operation:)` or `Task.detached(priority:operation:)` instead. /// /// - Parameters: /// - body: A closure that takes an `UnsafeCurrentTask` parameter. From 894c404fe7889d22af92e8c0966553425e1952b3 Mon Sep 17 00:00:00 2001 From: Jared Sinclair Date: Mon, 8 Dec 2025 17:27:42 -0500 Subject: [PATCH 2/3] Clarify unstructured Task documentation. --- stdlib/public/Concurrency/Task+init.swift.gyb | 14 +++--- stdlib/public/Concurrency/Task.swift | 46 +++++-------------- .../public/Concurrency/TaskCancellation.swift | 27 +++++++++++ 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/stdlib/public/Concurrency/Task+init.swift.gyb b/stdlib/public/Concurrency/Task+init.swift.gyb index 65c37db6a1554..ec121e26fdbcd 100644 --- a/stdlib/public/Concurrency/Task+init.swift.gyb +++ b/stdlib/public/Concurrency/Task+init.swift.gyb @@ -253,15 +253,13 @@ extension Task where Failure == ${FAILURE_TYPE} { % end /// % if IS_DETACHED: - /// In most cases, an "attached" unstructured task (created via `Task.init`) - /// is a more appropriate choice than a detached unstructured task. Attached - /// tasks inherit the priority and task-local storage from the enclosing task - /// during which they are created. You need to handle these considerations - /// manually with a detached task. - /// - /// A detached `Task` has the same cancellation mechanism as any other `Task`; - /// it can only become cancelled via an explicit `cancel()` method call. + /// A detached `Task` will not inherit task priority or task-local storage + /// from a parent task. You will need to handle these considerations manually. % end + /// + /// Asynchronous operations modeled with `Task` are ["unstructured concurrency"](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Unstructured-Concurrency). + /// Among other concerns, an unstructured task will not inherit cancellation + /// from a parent task. /// /// You need to keep a reference to the task /// if you want to cancel it by calling the `Task.cancel()` method. diff --git a/stdlib/public/Concurrency/Task.swift b/stdlib/public/Concurrency/Task.swift index c4526cdfd374c..0161481773958 100644 --- a/stdlib/public/Concurrency/Task.swift +++ b/stdlib/public/Concurrency/Task.swift @@ -77,32 +77,11 @@ import Swift /// This reflects the fact that a task can be canceled for many reasons, /// and additional reasons can accrue during the cancellation process. /// -/// A `Task` becomes cancelled when its `cancel()` method is called on it. There -/// is no other mechanism by which a `Task` can become cancelled. This is true -/// even of a `Task` instance which is created inside the operation of another -/// `Task`. You can manually propagate cancellation from an enclosing `Task` to -/// another task created during its operation via `withTaskCancellationHandler(operation:onCancel:isolation:)`: -/// -/// ```swift -/// let outer = Task { -/// let inner = Task { -/// // The next statement will not throw an error unless cancellation of -/// // the `outer` task is manually propagated to the `inner` task via a -/// // cancellation handler: -/// try Task.checkCancellation() -/// return try await work() -/// } -/// return try await withTaskCancellationHandler { -/// try await inner.value -/// } onCancel: { -/// inner.cancel() -/// } -/// } -/// -/// // When `outer` becomes cancelled, the `onCancel` handler above will run, -/// // causing `inner` to become cancelled, too. -/// outer.cancel() -/// ``` +/// An instance of `Task` becomes cancelled only when its ``Task/cancel()`` +/// method is explicitly invoked. No other mechanism can cancel a `Task`, +/// including any instance of `Task` created within the scope of another +/// cancelled task. To propagate cancellation from an enclosing task, provide a +/// cancellation handler via ``Task/withTaskCancellationHandler(operation:onCancel:isolation:)``. /// /// Any instance of `Task` that you initialize for yourself is a top-level, /// _unstructured_ task. For more information about the implications of @@ -232,16 +211,15 @@ extension Task { /// /// - It flags the task as canceled. /// - It causes any active cancellation handlers on the task to run, once. - /// - It cancels any child tasks (lowercase-t tasks, like `async let` and - /// other forms of structured concurrency, not capital-t `Task` instances) - /// and task groups of the task, including those created in the future. If - /// those tasks have cancellation handlers, they also are triggered. + /// - It cancels any child tasks (`async let` and other forms of structured + /// concurrency, not other `Task` instances) and task groups of the task, + /// including those created in the future. If those tasks have cancellation + /// handlers, they also are triggered. /// - /// Cancelling a `Task` does _not_ cancel other instances of `Task` that were + /// Cancelling a `Task` does not cancel other instances of `Task` that were /// or will be created during its operation. Those other instances will not /// become cancelled until and unless they are explicitly cancelled via - /// their `cancel()` methods being called on them. Read the "Task Cancellation" - /// section of the `Task` documentation for more information. + /// their `cancel()` methods being called on them. /// /// Task cancellation is cooperative and idempotent. /// @@ -711,7 +689,7 @@ extension Task where Success == Never, Failure == Never { /// To query the current task without saving a reference to it, /// use properties like `currentPriority`. /// If you need to store a reference to a task, create an unstructured task using -/// `Task.init(name:priority:operation:)` or `Task.detached(priority:operation:)` instead. +/// ``Task.init(name:priority:operation:)`` or ``Task.detached(priority:operation:)`` instead. /// /// - Parameters: /// - body: A closure that takes an `UnsafeCurrentTask` parameter. diff --git a/stdlib/public/Concurrency/TaskCancellation.swift b/stdlib/public/Concurrency/TaskCancellation.swift index b4d453e72651c..d8c584fade243 100644 --- a/stdlib/public/Concurrency/TaskCancellation.swift +++ b/stdlib/public/Concurrency/TaskCancellation.swift @@ -73,6 +73,33 @@ import Swift /// as resuming a continuation, may acquire these same internal locks. /// Therefore, if a cancellation handler must acquire a lock, other code should /// not cancel tasks or resume continuations while holding that lock. +/// +/// ### Propagating cancellation to unstructured tasks +/// +/// If an unstructured task (an instance of ``Task``) is created during the +/// operation of another task, cancellation of the enclosing task will not +/// propagate to the unstructured task without an explicit cancellation handler: +/// +/// ```swift +/// let outer = Task { +/// let inner = Task { +/// // The next statement will not throw an error unless cancellation of +/// // the `outer` task is manually propagated to the `inner` task via a +/// // cancellation handler: +/// try Task.checkCancellation() +/// return try await work() +/// } +/// return try await withTaskCancellationHandler { +/// try await inner.value +/// } onCancel: { +/// inner.cancel() +/// } +/// } +/// +/// // When `outer` becomes cancelled, the `onCancel` handler above will run, +/// // causing `inner` to become cancelled, too. +/// outer.cancel() +/// ``` @available(SwiftStdlib 5.1, *) #if !$Embedded @backDeployed(before: SwiftStdlib 6.0) From d87ce321e6ed10d8999e6d23a6cfd52fad0b542a Mon Sep 17 00:00:00 2001 From: Jared Sinclair Date: Mon, 8 Dec 2025 17:31:41 -0500 Subject: [PATCH 3/3] Address typo in Task.swift documentation link. --- stdlib/public/Concurrency/Task.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/public/Concurrency/Task.swift b/stdlib/public/Concurrency/Task.swift index 0161481773958..8a36482f58bc0 100644 --- a/stdlib/public/Concurrency/Task.swift +++ b/stdlib/public/Concurrency/Task.swift @@ -81,7 +81,7 @@ import Swift /// method is explicitly invoked. No other mechanism can cancel a `Task`, /// including any instance of `Task` created within the scope of another /// cancelled task. To propagate cancellation from an enclosing task, provide a -/// cancellation handler via ``Task/withTaskCancellationHandler(operation:onCancel:isolation:)``. +/// cancellation handler via ``withTaskCancellationHandler(operation:onCancel:isolation:)``. /// /// Any instance of `Task` that you initialize for yourself is a top-level, /// _unstructured_ task. For more information about the implications of