Isolation Assumptions

So then the answer to your question of “why wasn’t this promptly done?” seems obvious: because there has been no reason to assume it would be done at all.

huh? i was asking why patch releases for 5.9 were not made, not why 5.10 was not released.

That’s what I was answering. There’s no reason to assume that 5.9 patches would be made at all, much less “promptly”. Your question therefore comes off as fairly indignant.

It has always been the plan of record to stage in full strict concurrency checking in two phases: 1. basic actor isolation, and 2. full actor isolation. This plan was outlined in the concurrency roadmap long ago, and it has taken several years of development to reach the milestone of closing all known holes in the static data race checking model. It was well understood that closing these holes would require follow on language design proposals, as further communicated in the progress toward Swift 6 post from November from the Language Steering Group. The nature of these changes is not the same as "critical bug fixes", and it was clearly communicated that Swift 5.10 would contain these changes.

6 Likes

i don’t really like rehashing the history of the swift project, as i don’t think that’s productive. but there is some disconnect between what is optimal for Apple (a large corporation which has more flexibility and a deeper capacity to wait out supply chain disruptions) and smaller organizations that also depend on swift and don’t benefit from the same advantages as Apple.

i am pointing out that if the development of swift were not predominantly driven by a single, large corporation, that practices like tagging patch releases would be taken more seriously.

1 Like

When a Task { ... } is used in a context isolated to an actor instance, whether or not the task inherits the actor isolation depends on whether or not the actor value is captured, because the actor value must be captured in order to hop back to the actor after any suspension points. My understanding is the design decision was deliberate, and it was made to avoid implicitly capturing an isolated parameter when you didn't explicitly do so yourself. However, I believe at the time it wasn't understood that the decision had observable semantic implications because local non-Sendable state can still be accessed in a Task { ... } that's isolated to the actor. I completely agree that the current behavior is confusing, and we should change the behavior to always capture the isolated parameter.

7 Likes

i have misunderstood what is meant by “holes in the type checking”, as i was imagining something more akin to the runtime ARC bug that is also currently being tracked. i agree with your assessment that these are not critical bug fixes.

1 Like

I noticed a bug where Task { ... } did not inherit actor isolation when used in a function with an isolated parameter other than self even when the isolated parameter was explicitly captured:

// compiled with 5.10 under -strict-concurrency=complete

class NotSendable {}
actor MyActor{}

func testNonSendableCaptures(ns: NotSendable, a: isolated MyActor) {
  Task {
    _ = a
    _ = ns // warning: capture of 'ns' with non-sendable type 'NotSendable' in a `@Sendable` closure
  }
}

I've fixed this in [Concurrency] More precise modeling of `ActorIsolation` for isolated arguments and captures. by hborla · Pull Request #71143 · apple/swift · GitHub

4 Likes

I've put together a first draft of a real proposal. Thank you everyone for your help along the way!

The proposal differs pretty significantly from what was originally pitched. However, once it became clear that isolated parameters were nearly sufficient for solving the underlying problem, it seemed far more reasonable to just propose a small adjustment to them to get all the way there.

6 Likes

Very nice!

The proposed solution is to make this capture implicit when also capturing self, unless self is an actor type.

I would actually go one step further and propose that the Task initializer always inherits the enclosing isolated parameter (including both self in isolated actor methods and regular isolated parameters declared with the isolated parameter modifier), regardless of the captures in the task closure. This way, the rule is always consistent, and if programmers really want to not isolate that closure, we can add the ability to write nonisolated explicitly on closure expressions to deliberately prevent the inheritance.

7 Likes

Yes I agree with Holly here: it’ll be good for this to work consistently with just an isolated value — this way it’ll work with all kinds of contexts :+1: nice cleanup to Task{}.

3 Likes

I'm really glad you suggested this, because it really does make things easier to understand. I was hesitant to make such a change, but in the end it actually does not have a major impact on the overall design.

Proposal updated!

Edit: Hang on. I missed a critical part about the nonisolated keyword usage for a closure expression. Can you elaborate a little more here?

3 Likes

Sure! Consider this code:

actor MyActor {
  func inherit() {
    Task {
      // do some stuff but do not capture 'self'
    }
  }
}

With the new proposal, that Task will always be isolated to self. Say somebody wants to deliberately prevent that isolation inheritance, but they don't want to use Task.detached because they still want to inherit priority. There's no way to explicitly say that a closure is nonisolated so this code would have to be rewritten to use a local function that is nonisolated. Instead, we could allow nonisolated to be written in a closure signature:

actor MyActor {
  func inherit() {
    Task { nonisolated in
      // this task is nonisolated
    }
  }
}

I also think this can be a future direction of the proposal.

4 Likes

Proposal updated again, now with the suggested closure expression grammar changes needed.