I'm processing feedback from the first iteration:
So, in the updated proposal current behaviour will be preserved by default, and isolation will require explicit opt-in.
For actors, syntax would be isolated deinit {
.
For GAIT, there are two options. One can similarly use attribute isolated
to opt-in into inheriting global actor from the class. Alternatively one can explicitly repeat global actor declaration:
class MyView1: UIView {
isolated deinit { ... }
}
class MyView2: UIView {
@MainActor deinit { ... }
}
I think the first option is more preferable, because it follows DRY principle. So, it will be recommended in the proposal text, any updates to the Swift book should use it, and in case of ambiguity it will be chosen for fix-its.
It is an error to use isolated
if there is no global actor isolation to inherit. It is an error to combine isolated
attribute with nonisolated
or global-actor isolation attribute.
I'm willing to push back on this. I think it is a premature optimization. I see value in having access to the task locals in the deinit
. If object is referenced by several child tasks, you cannot know which one will make last release, so you cannot reliably use values set in child tasks. But as long as you are confident that object does not escape scope of the parent task, you can still reliably use values set by the parent task.
If task locals are used to inject logger/error tracker, it is easy to forget to reinject it in the deinit
. This will cause errors occurring inside the deinit to be not visible in the dashboards. And if errors are rare and unexpected, both problems (error itself and broken monitoring) can remain undiscovered for a long time.
class ErrorLogger {
@TaskLocal
static var instance: ErrorLogger? = nil
func log(_ error: Error) { ... }
}
@MainActor
class Resource {
isolated deinit {
shutdown()
}
}
func shutdown() {
do {
...
} catch {
ErrorLogger.instance?.log(error)
}
}
And also note that performance cost for copying task locals occurs only when hopping is needed. If last release occurs on the right actor, no copying happens. Which I expect to be pretty common.
To support demand for blocking copying task-locals in isolated deinit, I can suggest to use an attribute:
@MainActor
class Resource {
let errorLogger: ErrorLogger
@detached
isolated deinit {
EventLogger.$instance.withValue(errorLogger) {
shutdown()
}
}
}
Attribute @detached
implies analogy between Task
vs Task.detached
. Which is a good analogy in regards to task-locals, but Task
vs Task.detached
also differs in priority inheritance. I don't recall seeing much demand for priority management for isolated deinit, so it might be better to use different attribute name to avoid overpromising.
If implementing blocking task-locals, I can also add public API for that, something like this:
public func withResetTaskLocalValues<R>(
operation: () async throws -> R,
file: String = #fileID, line: UInt = #line
) async rethrows -> R
And one could use this function to achieve blocking task-locals deinit as a functional requirement, but not for performance reasons:
// When hopping, task-locals are first copied
isolated deinit {
// ... and then blocked
withResetTaskLocalValues {
...
}
}
As an alternative to @detached
attribute, it is possible to provide a guaranteed optimization that recognises this pattern and blocks task-locals in the swift_task_deinitOnExecutor
.
Async deinit is a big topic, I'm gonna make a separate thread for it.