Skip to content

Commit a62c1f2

Browse files
committed
update semantics of static and member methods
1 parent 5cc62f1 commit a62c1f2

File tree

1 file changed

+105
-22
lines changed

1 file changed

+105
-22
lines changed

proposals/NNNN-task-cancellation-shields.md

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ Task cancellation shields directly resolve these problems.
7373
We propose the introduction of a `withTaskCancellationShield` method which temporarily prevents code from **observing** the cancellation status, and thus allowing code to execute as-if the surrounding task was not cancelled:
7474

7575
```swift
76-
public func withTaskCancellationShield<T, E>(
77-
_ operation: () throws(E) -> T,
76+
public func withTaskCancellationShield<Value, Failure>(
77+
_ operation: () throws(Failure) -> Value,
7878
file: String = #fileID, line: Int = #line
79-
) throws(E) -> T
79+
) throws(Failure) -> Value
8080

81-
public nonisolated(nonsending) func withTaskCancellationShield<T, E>(
82-
_ operation: nonisolated(nonsending) () async throws(E) -> T, // FIXME: order of attrs
81+
public nonisolated(nonsending) func withTaskCancellationShield<Value, E>(
82+
_ operation: nonisolated(nonsending) () async throws(Failure) -> Value,
8383
file: String = #fileID, line: Int = #line
84-
) async throws(E) -> T
84+
) async throws(Failure) -> T
8585
```
8686

8787
Shields also prevent the automatic propagation of cancellation into child tasks, including `async let` and task groups.
@@ -124,7 +124,7 @@ Task {
124124
}
125125
```
126126

127-
However if a child task were to be cancelled explicitly the shield of the parent, has no effect on the child itself becoming cancelled:
127+
However if a child task (or entire task group) were to be cancelled explicitly, the shield of the parent task has no effect, as it only shields from "incoming" cancellation from the outer scope and not the child task's own status.
128128

129129
```swift
130130
await withTaskCancellationShield {
@@ -152,6 +152,8 @@ await withDiscardingTaskGroup { group in
152152
}
153153
```
154154

155+
All examples shown using `isCancelled` behave exactly the same for `Task.checkCancellation`, i.e. whenever `isCancelled` would be true, the `checkCancelled` API would throw a `CancellationError`.
156+
155157
### Cancellation Shields and Cancellation Handlers
156158

157159
Swift concurrency offers task cancellation handlers which are invoked immediately when a task is cancelled. This allows you to dynamically react to cancellation happening without explicitly checking the `isCancelled` property of a task.
@@ -171,11 +173,42 @@ func slowOperation() -> ComputationResult {
171173

172174
func cleanup() {
173175
withTaskCancellationShield {
174-
176+
slowOperation()
177+
}
178+
}
179+
```
180+
181+
### Cancellation Shields and Task handles
182+
183+
Unstructured tasks, as well as the use of `withUnsafeCurrentTask`, offer a way to obtain a task handle which may be interacted with outside of the task.
184+
185+
For example, you may obtain a task handle a an unstructured task, which then immediately enters a task cancellation shield scope:
186+
187+
```swift
188+
let task = Task {
189+
Task.isCancelled // true
190+
withTaskCancellationShield {
191+
Task.isCancelled // false
175192
}
193+
Task.isCancelled // true
176194
}
195+
196+
task.cancel()
197+
print(task.isCancelled) // _always_ true
177198
```
178199

200+
The **instance method** `task.isCancelled` queried from the outside of the task will return the _actual_ cancelled state, regardless if the task is right no executing a section of code under a cancellation shield or not. This is because from the outside it would be racy to query the cancellation state and rely on wether or not the task is currently executing a section of code under a shield. This could lead to confusing behavior where querying the same `task.isCancelled` could be flip flopping between cancelled and not cancelled.
201+
202+
The **static method** `Task.isCancelled` always reports the cancelled status of "this context" and thus respects the structure of the program with regards to nesting in `withTaskCancellationShield { ... }` blocks. Therefore the static `Task.isCancelled` method is always returning the actual cancellation status (regardless of installed shields).
203+
204+
The static method was, and remains, the primary way tasks interact with cancellation.
205+
206+
We believe these semantics are the right, understandable, and consistent choice of behavior:
207+
208+
- **instance methods** on `Task` (and `UnsafeCurrentTask` discussed next) observe cancellation observe the "actual" cancellation state, since they may be queried from any context.
209+
- **static methods** observe the cancellation status "in this context", and thus, respect task cancellation shields,
210+
- This includes the: `Task.isCancelled`, `Task.checkCancellation` and `withTaskCancellationHandler` methods.
211+
179212
### Debugging and Observing Task Cancellation Shields
180213

181214
While it isn't common to explicitly cancel the current task your code is executing in, it is possible and may lead to slightly unexpected behaviors which nevertheless are correct. For example, if attempting to cancel the current task while it is running under a cancellation shield, that cancellation would not be able to be observed, even in the next line just after triggering the "current task" cancellation:
@@ -190,33 +223,75 @@ withTaskCancellationShield {
190223

191224
While this code pattern is not really often encountered in real-world code, it could confuse developers unaware of task cancellation shields, especially in deep call hierarchies.
192225

193-
In order to aid understanding and debuggability of cancellation in such systems, we also introduce two new query functions:
226+
In order to aid understanding and debuggability of cancellation in such systems, we also introduce a new property to query for a cancellation shield being active in a specific task.
194227

195-
First, the `isTaskCancellationShielded` static property, which can be used to determine if a cancellation shield is active. Primarily this can be used for debugging "why isn't my task getting cancelled?" kinds of issues.
228+
This API is not intended to be used in "normal" code, and should only be used during debugging issues with cancellation, to check if a shield is active in a given task. This API are _only_ available on `UnsafeCurrentTask`, in order to dissuade from their use in normal code.
229+
230+
The `hasActiveTaskCancellationShield` static property, which can be used to determine if a cancellation shield is active. Primarily this can be used for debugging "why isn't my task getting cancelled?" kinds of issues.
196231

197232
```swift
198-
extension Task where Success == Never, Failure == Never {
199-
public static var isTaskCancellationShielded: Bool { get }
200-
// TODO: or hasActiveTaskCancellationShield ???
233+
extension UnsafeCurrentTask {
234+
public static var hasActiveTaskCancellationShield: Bool { get }
201235
}
236+
```
237+
238+
Here is an example, how `UnsafeCurrentTask`'s `isCancelled` as well as the new `hasActiveTaskCancellationShield` behave inside inside of a cancelled, but shielded task. The instance method `UnsafeCurrentTask.isCancelled` behaves the same way as the `Task.isCancelled` method, which was discussed above. However, using the unsafe task handle, we are able to react to task cancellation shields if necessary:
202239

203-
extension UnsafeCurrentTask where Success == Never, Failure == Never {
204-
public var isTaskCancellationShielded: Bool { get }
240+
```swift
241+
let task = Task {
242+
Task.isCancelled // true
243+
244+
withTaskCancellationShield {
245+
Task.isCancelled // false
246+
247+
withUnsafeCurrentTask { unsafeTask in
248+
unsafeTask.isCancelled // true
249+
unsafeTask.hasTaskCancellationShield // true
250+
251+
// can replicate respecting shield if necessary (racy by definition, if this was queried from outside)
252+
let isCancelledRespectingShield =
253+
if unsafeTask.hasTaskCancellationShield { false }
254+
else { unsafeTask.isCancelled }
255+
}
256+
}
205257
}
258+
259+
task.cancel()
260+
print(task.isCancelled) // true
206261
```
207262

208-
As well as, a version of `isCancelled()` which allows ignoring the cancellation shield:
263+
### Modifying the `isCancelled` behavior contract
264+
265+
Previously, the static `Task.isCancelled` property declared on Task was documented as:
209266

210267
```swift
211-
extension Task where Success == Never, Failure == Never {
212-
public static func isCancelled(ignoringCancellationShield: Bool) -> Bool
213-
}
214-
extension UnsafeCurrentTask where Success == Never, Failure == Never {
215-
public func isCancelled(ignoringCancellationShield: Bool) -> Bool
268+
/// After the value of this property becomes `true`, it remains `true` indefinitely.
269+
/// There is no way to uncancel a task.
270+
```
271+
272+
With cancellation shields, this wording may be slightly confusing. It is true that cancellation is terminal and cannot be "undone", however this proposal does allow an `isCancelled` on a task that previously returned `true` to return `false`, if and only if, that task has now entered a task cancellation shield scope:
273+
274+
```swift
275+
Task.isCancelled // true
276+
withTaskCancellationShield {
277+
Task.isCancelled // false
216278
}
279+
Task.isCancelled // true
280+
```
281+
282+
Therefore the API documentation will be changed to reflect this change:
283+
284+
```swift
285+
/// ...
286+
/// A task's cancellation is final and cannot be undone.
287+
/// However, is possible to cause the `isCancelled` property to return `false` even
288+
/// if the task was previously cancelled by entering a ``withTaskCancellationShield(_:)`` scope.
289+
/// ...
290+
public var isCancelled: Bool {
291+
217292
```
218293

219-
This overload should not really be used by normal code trying to act on cancellation, and we believe the long name should indicate as much, as will the documentation on those methods. However, we believe offering it is may be beneficial for certain code paths which can benefit from seeing the whole picture, and/or software logging and reporting statuses of tasks etc.
294+
The instance method `task.isCancelled` retains its existing behavior.
220295

221296
### Compatibility with defer
222297

@@ -252,6 +327,14 @@ Doing nothing is always an option, and we suggest developers have to keep using
252327

253328
This doesn't seem viable though as the problem indeed is real, and the workaround is problematic scheduling wise, and may not even be usable in certain situations.
254329

330+
### Naming the feature "ignore cancellation" or similar
331+
332+
During the pitch a variety of name alternatives for this feature were proposed. Among them were "`ignoringCancellation { ... }`", or "`suppressingCancellation{ ... }`".
333+
334+
We discussed these and believe it is _more_ confusing to introduce a descriptive name for this feature because the descriptions never _quite_ capture the actual feature's behavior, and would give a false sense of understanding without looking up API docs and/or extended documentation explaining the behavior.
335+
336+
Specifically, this feature does _not_ ignore cancellation, it only prevents observing it while within the scope of a shield within a task.
337+
255338
## Acknowledgments
256339

257340
The term cancellation "shield" was originally coined in the Trio concurrency project, and we think the term is quite suitable and well-fitting to Swift as well.

0 commit comments

Comments
 (0)