You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Shields also prevent the automatic propagation of cancellation into child tasks, including `async let` and task groups.
@@ -124,7 +124,7 @@ Task {
124
124
}
125
125
```
126
126
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.
128
128
129
129
```swift
130
130
awaitwithTaskCancellationShield {
@@ -152,6 +152,8 @@ await withDiscardingTaskGroup { group in
152
152
}
153
153
```
154
154
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
+
155
157
### Cancellation Shields and Cancellation Handlers
156
158
157
159
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.
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
175
192
}
193
+
Task.isCancelled// true
176
194
}
195
+
196
+
task.cancel()
197
+
print(task.isCancelled) // _always_ true
177
198
```
178
199
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
+
179
212
### Debugging and Observing Task Cancellation Shields
180
213
181
214
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:
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.
192
225
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.
194
227
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.
publicstaticvar isTaskCancellationShielded: Bool { get }
200
-
// TODO: or hasActiveTaskCancellationShield ???
233
+
extensionUnsafeCurrentTask {
234
+
publicstaticvar hasActiveTaskCancellationShield: Bool { get }
201
235
}
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:
/// 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
216
278
}
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
+
publicvar isCancelled: Bool {
291
+
217
292
```
218
293
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.
220
295
221
296
### Compatibility with defer
222
297
@@ -252,6 +327,14 @@ Doing nothing is always an option, and we suggest developers have to keep using
252
327
253
328
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.
254
329
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
+
255
338
## Acknowledgments
256
339
257
340
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