[Returned for revision] SE-0371: Isolated synchronous deinit

Hi all,

The review for SE-0371: "Isolated synchronous deinit" ran from August 18th to August 31st. The language workgroup has decided to return the proposal for revision.

Feedback was overall positive on solving the issue presented in the proposal, and there was no major disagreement around the semantics of the isolated deinit. However, there were some concerns around defaulting deinits to being isolated and around the copying of task locals. Specifically, reviewers pointed out that defaulting deinit to isolated may introduce unnecessary hops (in addition to binary size and runtime costs), and that copying task locals was a potentially expensive operation that was not likely to be useful in practice. The workgroup did recognize, however, that there is value in having deinit receive a consistent view of the task locals, rather than just receiving the task locals of whatever task happens to run the deinit.

There were also questions raised about how this feature might interact with a future, fully-fledged deinit async feature, and whether such a feature might subsume the need for special-case rules for isolated deinit by allowing us to align init and deinit isolation rules. The workgroup discussed these points but realizes that there is likely a lot of further design work that would go into the full deinit async feature, so it isn’t entirely clear whether or not it would be worth it to have a way to write an isolated deinit in the meantime.

For these reasons, the language workgroup has elected to request the following revisions:

  • By default, deinit should behave the same as today, that is, it should be nonisolated and only able to access Sendable state. To achieve an isolated deinit, the user should have to opt-in explicitly. One syntax for this opt-in suggested in the review was isolated deinit.
  • Task locals should not be copied for an isolated deinit. Instead, to ensure the deinit receives a consistent set of task locals, the task locals should be cleared before calling into the deinit.
  • Inclusion of additional discussion around the future deinit async direction. The language workgroup is not certain whether it makes sense to delay the ability for deinit to access isolated state until the implementation of a significantly larger and more complex feature, but would still like to see a more thorough discussion of this future direction to help make the case that it is worth it to add isolated deinit now.

Once these revisions are made the language workgroup will run another review of the revised proposal.

Thank you for participating in Swift Evolution.

12 Likes

May I ask if there is any progress / priority / estimation around this?

If I understand it correctly, there is currently no way how to gracefully shutdown pending items isolated to the actor in deinit?

These may by pending URL tasks, opened file handlers, locks, closures that require to call a completion, expensive executions, files to be deleted, memory to be freed, potentially notifications / eventns to be fired or even observers and schedulers to be cancelled (unowned, indirect). Lots of stuff... Is there a workaround to this or why aren't people concerned about it?

4 Likes

Also curious about this. I sometimes have long-running Tasks in my actor that I'd like to shutdown without resorting to a manual shutdown method of some sort.

I am concerned about it. The inability to cancel some long running/infinite tasks has already caused a few problems for me today. I hope this feature comes soon.

1 Like

I just ran into this too and would love this to be completed.

1 Like

Very pleased to see there's a potential solution to resolve this.

With Actors promoted as a way to 'protect shared mutable state', in practice that often means a wrapper for a bunch of non-Sendable conforming types. So, the requirement that members of an actor must also be Sendable to be accessible in the deinit is rather non-intuitive.

I think this is fair enough, but I have to say the asymmetry around isolation between a types init/deinit and its members is pretty confusing and seems inconsistent. The example given for actor initializers in the review shows that actor initializers are, by default, non-isolated, and therefore we should follow the same example for deinit.

And yet, for GAITs, it seems we are isolated:


@MainActor class GAIT {
  
  let x: Int
  
  init(x: Int) {
    self.x = x
    doStuff() // No sweat.
  }
  
  func doStuff() {
    print("x: \(x)")
  }
}

So it seems by the logic of the review, we should at least default GAITs to have isolated deinits.

More broadly though, I wish there was someway of getting consistency across these various types. It seems that the most intuitive solution would have been to require actors and GAITs to mark their init/deinits with nonisolated if that was indeed the context they'd be called in – leaving the defaults to be isolated (or not available) – but perhaps that ship has now sailed.

2 Likes