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.
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.
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.
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.
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
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.
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.
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 nice cleanup to Task{}.
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?
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.
Proposal updated again, now with the suggested closure expression grammar changes needed.
I have posted an expanded and merged pitch here.
Going through the thread, I’ve got one more idea, which is related, but probably deserves a separate proposal.
So far, discussion have reached a stage where original problem is solved by passing isolating actor as a parameter.
But what if that is not possible. What if we want to infer the isolating actor from self
. If reference is isolated to a connected region from the moment of allocation, it should be possible.
And it can be expressed using an initializer isolated to an actor parameter.
But we somehow need to pass that isolated actor parameter from initializer to the instance method.
My idea is to allow isolated
keyword to be applied to stored read-only properties:
class NonSendableType {
private var internalState = 0
private let actor: isolated (any Actor)
init(actor: isolated (any Actor)) {
self.actor = actor
}
init(another: (any Actor)) {
self.actor = another // Error: isolated stored property is initialized by a non-isolated value
}
func doSomeStuff() {
// self is known to be isolated to self.actor
// so current method must be isolated to self.actor too
Task {
let value = await otherType.getAValue()
// hop back to self.actor
self.internalState += value // ok
}
}
}
I think this is a fascinating direction to pursue! I think anything that offers greater flexibility around isolating non-Sendable types is worth exploring.
I do think a different discussion thread is warranted though, especially this one has now already been retired in favor of a much better-designed proposal.
Yeah, I think this is exactly what @Jumhyn was describing here:
I think it's an idea worth exploring, but we would probably need a way to preserve when you've isolated a non-Sendable
stored property of an actor to self
in that actor's initializer, so that calls to async
functions on that non-Sendable
value in isolated actor methods can statically understand that no isolation boundary is crossed.
+1 on the general case of being able to pass isolation to the class.
What I see as a potential cases from design perspective is that in order to allow that class should explicitly provide that ability with implementation. So if its your class, for each such case you need to add this actor dependency and think on how to pass it. If its class you have no control over implementation, it is impossible to address this at all.
So what I invision as a solution is to allow explicitly set isolation on instances of that class, at the current stage it is something like the following:
final class NonSendableType {
// assertion just for the demonstration purposes
let assertion: () -> Void
func doSmth() async {
assertion()
}
}
@MainActor
struct Command {
@isolated(MainActor.shared)
let instance = NonSendableType {
MainActor.assertIsolated()
}
func execute() {
Task {
await instance.doSmth()
}
}
}
I do not like isolation being expressed as part of a property, because it seems wrong to express isolation in that way, it's more like just this property being isolated... but hasn't came up with better idea so far.
But the end goal is to allow isolation to be - ideally, assumed, but at least to be explicitly specified - at the usage point.
We definitely could allow methods and properties to be isolated to a specific immutable stored property. I'm concerned that it might prove to be a deceptively large feature, though.