Not in this actor proposal, no. Global actors (formerly part of this proposal, to be split out into a separate proposal with more exposition) provide some global coordination.
Thanks, that's very illuminating. I do worry about the complexity of needing to use detached tasks for these sort of things, but that may just be a matter of getting used to the pattern.
Ah, so @actorIndependent(unsafe) isn't necessarily unsafe, it just makes the guarantees manual. So if I have read-only properties that present values from private mutable state, wrapped in a lock, would this work?
// Inside an actor.
@actorIndependent(unsafe)
public var state: State {
mutableState.state
}
@Protected // Property wrapper providing property access through os_unfair_lock
private var mutableState: MutableState
Would that be enough to offer a safe, synchronous value (assuming Protected works)? Or would there be another pattern to use here? And would there be a way to differentiate internal access to the property from external and avoid the locking?
Right. I'm mostly concerned that adopting actors to use async internally means all of my users have to use async externally, for everything. And I wonder how hard that's going to be. Given the prevalence of types that could benefit from compiler-guaranteed thread-safety, aren't we going to see async everywhere?
Yes, I'm intentionally leaving this as "functions" until we get more direction on the effect-ful properties bit. The += here is treating otherActor.state as inout, so I think it should be banned by the wording in this proposal that prevents inout with asynchronous calls.
It's most everything a class can do. The core data ones won't work because they depend on inheriting from NSManagedObject (which actors can't do). Anyway, I can document this. It was way easier when we had actor class ;)
@concurrent closures must be actor-isolated lest we create races. Non-@concurrent closures might or might not be actor-isolated. You surely want the ones passed to Sequence.map to be actor-isolated so you can access your own state within a sequential algorithm. (You note this with your forEach example as well)
Sure, but relying entirely on @concurrent leaves significant holes in the model if you try to tie it exactly to actor-independent. Existing APIs that take escaping closures and run them concurrently abound in Swift:
func runLater(_: @escaping (Int) -> Void) { ... }
actor MyActor {
var numbers: [Int] = []
func f() {
runLater { // non-concurrent, therefore actor-isolated in your proposal
numbers.append($0) // race condition
}
}
}
This is why @ktoso and I keep bringing up the "escaping implies not actor-isolated" approach in different forms. Perhaps we can tackle it directly by saying that escaping closure is always actor-independent.
It's frustrating to repeat my point and have it come back unrecognizably altered. It's not conflating the two, it is noting that we'd be leaving a large gap in the new model if one can escape an actor-isolated closure (where it will certainly be run concurrently) due to the lack of a @concurrent annotation in existing code. We've repeatedly noted that parallelForEach is an example of "non-escaping, concurrent", and the discussion is about spelling this rarer case as @nonconcurrent so that the defaults are right.
Actor-independent declarations are concurrent with respect to actor state, yes.
Sure.
I see how you can interpret that as giving people false hope. I realize now that we don't make the reference-cycle analogy in the document, but we should. It's a similar issue.
Huh, that's interesting. We might end up with a significant numbers of folks considering that to be an untenable middle ground, where the actors proposal is "unacceptable" without the ability to disable reentrancy. I do agree that we're in a better place now that we have an actual definition of what this attribute would look like, although I'd also love to have a prototype in place as well.
I'm not sure what you're referring to with "member lookup." AnyObject lookup occurs on values of type AnyObject, not on the self of a class type. We should probably ban references to synchronous @objc functions on an actor, whether via AnyObject lookup, #selector, or #keyPath, but otherwise I don't see a benefit to removing the ability to inherit NSObject. At the very least, we would want to be able to have actors provide NSObjectProtocol conformance (which we currently get through NSObject conformance) so that one can have an actor conform to various delegate protocols---which often have requirements that will come in as async or can be handled by performing the effects within a detached task.
I agree that this should be nailed down further. I'd rather keep it in the proposal (perhaps in its own section on ObjC interoperability later), because I think it's short and I'm running out of juggling hands.
This is an interesting point. Rather than have this be an annotation on the actor or an actor-isolated function, it could be an annotation on the await itself, because it's really the specific asynchronous calls that would be reentrant (or not).
This assumes that you can see the body of every function that's in the call chain. That's true within a single source file and potentially within a single module (if you accept the compile-time costs of whole-module-optimization all the time), but it falls apart across modules. We cannot make that assumption for our arguments.
I do not think we should assume that we will get any compile-time mechanism to prove the existence or lack of deadlocks. The deadlocks that occur in practice will involve much more code than the compiler can see. At best, we can have runtime tools to identify the deadlock when it happens at run time, akin to detecting a reference cycle at runtime.
As I noted earlier, await is still important to note that your function might get suspended, and the world may move on (affecting global state, timers, etc.) before you resume.
A detached task is essentially a future. Also note that my example generalizes to a generic cache of asynchronous operations, e.g.,
actor Cache<Key: Hashable, Value> {
// The body performs the computation for a key.
init(body: (Key) async throws -> Value) { ... }
func getValue(_ key: Key) async throws -> Value { ... }
}
let imageDownloader = Cache<URL, Image> { data in
let data = await download(url)
return await Image(decoding: data)
}
Maybe I should add that example to the proposal...
You might need another @actorIndependent(unsafe) on mutableState, but it might be possible. We'd have to sort out how the property wrapper type and the actor interact when synthesizing the computed properties in the actor type. This sounds like something @hborla could weigh in on.
By "internal" I think you mean "on self". You might be able to pull some tricks where the property wrapper has a projectedValue that's synchronous and not actor-independent, but your question seems to assume that it's safe to skip the lock when you're running in actor-isolated code. That could be true for reads in actor-isolated code, if you also ensure that writes are only possible from actor-isolated code (and, of course, need to take the lock).
I do expect we'll see a bunch of async, especially in fundamentally asynchronous areas like networking or user interfaces. I don't feel like your question has an answer beyond "let's write some code and see how you feel."
Right. The design in the @concurrent closure doc says that @concurrent closures are guaranteed to be actor isolated (i.e., they could be from a different concurrency domain) and non-@concurrent closures are guaranteed to be usable within the same actor. This should work completely safely by composition of the rules described there.
The only point I was trying to make is that the writing will be completely different if/when we take some form of the @concurrent attribute for closures. That design subsumes the whole escaping discussion for example (regardless of which way the design is resolved towards).
Yes, I agree that this is a serious issue with detailed tradeoffs. I think we have to decide how important this is and how problematic it is to accept. My belief (which is not based on data) is that all such code will be correct in the new model or is already buggy. This is by virtue of the fact that there has to be explicit synchronization in the code to make it safe in Swift 5.
There is some risk that adopting actors and structured concurrency into a legacy codebase will miss the invariants on that code, but I don't think it is a significant risk. I'd love to see more real motivating examples.
In any case, it is very possible to make @concurrent be a tower that implies @escaping, but it would be better to make it orthogonal if we can in my opinion, because it allows correct modeling for things like parallelMap and escaping-but-non-concurrent closures.
I'm sorry if this came across the wrong way, but by "conflating" I meant it was conflating "escaping with concurrent". As I tried to show above, there are concurrent closures that are non-escaping, and escaping closures that are non concurrent. Mapping the two concepts together will lead to modeling problems that will make lead to the need to use unnecessary awaits and unnecessary runDetatched calls. It would be preferable to avoid mixing these two different concepts if possible, for the same reason we don't want to conflate error handling and async.
Thanks, just to be clear, I think you're capturing the right idea, this was just a wording suggestion. This is a complicated proposal that will be read by a wide audience, so I'm just trying to help make sure the message is clear. I didn't mean to come across as nit-picking you. Thank you!
Yes, I think that is a significant concern, but I think it is addressable by saying "we have a fleshed out design in future work; we are confident we can add this if it becomes an issue in practice". This approach has often been helpful in ensuring that diligence has been applied to make sure the future direction hasn't been cut off, while not opting into the complexity until we have usage experience.
I think the design you've come up with is quite reasonable if we have to do something like this, I just hope we don't
actor Foo : NSObject {
func someMethod() {
// This shows such exciting thing as mutableCopy, instancesRespond(to: Selector!), autoContentAccessingProxy, classForArchiver, ....
self.<tab>
}
}
func useFoo(a: Foo) {
x.<tab>
}
If instead you say that "the way to interface with ObjC is with the @objc attribute" then you don't pollute the namespace with all this stuff. The actor can still conform to NSObjectProtocol if they want to. Instead of the above subclass, you'd just have @objc actor Foo { ... }.
Such a design also seems more consistent with the rest of the ObjC interop: we don't require Swift classes to subclass NSObject to bridge (though that is also allowed for classes of course), we only require them to be tagged with @objc.
Fair enough, I would be happy if you just switched this reference to say "Use @objc to expose an actor to Objective-C bridging, just like classes".
Thanks Doug! (very punny)
I think that he is suggesting that we retain marking in such a case, but instead of spelling the mark as await, it is spelled something else (e.g. block or await_blocking etc). I tend to agree - the attribute that changes behavior may be in a far off place lexically, but the effect is at the site of the mark. Making it obvious in the marking would make the behavior more clear for readers of the code in a really large actor whose attribute is on the actor decl itself.
Say I use a ThirdPartyLibA.Actor, which itself uses a ThirdPartyLibB.Actor
I assume there's no way to instantiate a ThirdPartyLibA.Actor so that it runs its stuff on my global actor (without forking ThirdPartyLibA), and even if there was, the ThirdPartyLibA.Actor has no clue that it should be forwarding that request to its ThirdPartyLibB.Actor
It's funny because this prompted me searching for alternatives to @reentrant(never), and I ended up finding that you can deadlock an actor without it.
Hastily written so please excuse any syntax error, but this will deadlock right?
actor Deadlock {
var task: Task.Handle<Int>?
func getValue() async -> Int {
if task == nil {
task = detachedTask {
return await computeValue()
}
}
return await task!.get()
}
private func computeValue() async -> Int {
return await getValue() + 1
}
}
Technically, only the getValue member is deadlocked; other parts of the actor not depending on getValue or computeValue would still run. In practice it might not make much of a difference.
For reference, the @reentrant(never) equivalent would look like this:
actor Deadlock {
var value: Int?
@reentrant(never)
func getValue() async -> Int {
if value == nil {
value = await computeValue()
}
return value!
}
private func computeValue() async -> Int {
return await getValue() + 1
}
}
If the overall design is desirable but this is a sticking point, Iām a big proponent of the parse-both-ways strategy of transitioning these changes in.
(This applies equally to the already-accepted async in ... syntax.)
A closure that intends to use this name for its sole parameter can be expected to refer to it within the body of the closure and not refer to $0, etc. Iām sure that there are some wrinkles I havenāt thought through in the case of nested closures, etc., but if feasible from an implementation standpoint, trying to parse as an attribute and falling back to parsing as a parameter name if we encounter an otherwise undeclared variable of the same name should preserve source compatibility in all but the most unusual casesāand again this can be a transitional measure with a fix-it thatās taken out after one major version.
There are no mentions of this contracted syntax in the accepted SE-0296 and { throws in } never worked as I'm aware. What does work is { throw MyError } which implicitly infers a throws closure type (analogously, { await 3 } implicitly infers an async closure type and this is explicitly stated in the proposal).
Hmm. If I understand the actor isolation model correctly, I don't think you would need another @actorIndependent(unsafe) on the wrapped property (although we should still figure out what it means when you do mark a wrapped property as @actorIndependent(unsafe)). I think all of the declarations associated with the wrapped property would be actor-isolated by default, so it's fine for the computed mutableState instance property to synchronously access _mutableState.wrappedValue, unless the property wrapper is an actor and the wrapped value is actor-isolated. Unsafe actor-independent declarations are allowed to synchronously access actor-isolated declarations, so I think what @Jon_Shier wrote is sufficent for providing safe, synchronous read access to the value as long as the backing property wrapper itself can't change (which seems like a big "if"). It would be helpful if the property wrapper storage could be a let constant -- then both public var state and @Protected private var mutableState could be @actorIndependent.
If you don't mark the wrapped property as @actorIndependent(unsafe), then writes are still only possible in actor-isolated code, which I think addresses this point:
It doesn't work completely safely because of escaping closures.
My opinion is based on porting code to the new model, and escaping closures that attempt to access actor state came up fairly often where the compiler caught me. Some of it is porting detritus, e.g., a myQueue.async { ... } in a class that's getting ported to an actor. The more persistent issues come up when setting a callback somewhere that calls back into the actor, e.g., wiring up a subscriber into a reactive streams library, setting up an event handler in a UI or in reaction to some other external event. All of those common patterns will continue to use @escaping, and relying on folks to go back and ensure that those places gain a @concurrent to prevent data races on actors undercuts the whole model. I consider this problem to be an order of magnitude worse than the one solved by ConcurrentValue.
I doubt it will be that different, so long as it's not trying to solve both problems at once and can reference a definition of a concurrent closure/local function elsewhere, the rule is: if the place where self is captured is concurrent or escaping, it's independent of the actor.
As I've noted repeatedly, this is not what I'm proposing.
Yes, I know, but you're arguing against a position I haven't ever held. I described the reasons for each cell in the escaping/concurrent matrix specifically over here.... 23 days ago.
There are very interesting questions about @concurrent functions and closures to resolve that are getting drowned out by this. Fortunately, most of the implementation is straightforward, including how mutating checking ties in with the "may execute concurrently with" checks from the "Preventing Data Races" document. But these are better dealt with in the other thread.
Right now, if you declare an @objc class, it must inherit from NSObject. That's also the only way to conform to NSObjectProtocol. And the latter brings in most of the legacy methods, so I don't know that we'll fix much by making actors different from classes in this regard.
You can declare @objc members without inheriting from NSObject, of course. I think that's what you're remembering.
I'm not sure what you mean, can you please provide an example? The threads have gone back and forth on various points, and I'd like to make sure I understand what you're saying.
Sure, I can see how that is a problem, but as I mention we need to look at the space of alternative solutions along with their implications. Making @escaping imply @concurrent would have obvious downsides: it would significantly muddy the user water, causing users to have to await non-concurrent things, break the ability to cleanly model core features like parallelMap, etc. (see comment below about me not trying to convince you of things you already believe).
There are alternate design points that seem worthy of consideration, including importing Swift5 module interfaces into Swift6 code with magic attributes, adding Clang attributes that cause C closures to be imported into Swift as @concurrent for key APIs etc. It is possible of course that these paths have been evaluated, but I haven't seen any public evidence to show this and think it is important. This is important because making escaping closures implicitly concurrent has obvious negative impact on the model, and we need to balance tradeoffs here.
I'm not sure if this is an offhand comment or something that I should address, but without ConcurrentValue you don't achieve the goal of race safety and have a huge footgun in the language.
I'm not sure how you can claim this. The writing would obviously reference ConcurrentValue and most references to closures would be defined away by referring to @concurrent in one place.
Sounds good, and yes, I have seen your argument on the other thread. Just to clarify my intent here: when I post, I expect it to be ready by a wide variety of people with different positions, not just you. This is why I try hard to be very clear without biasing the wording to (my current understanding of) your position in particular. I'm sure if this seems redundant or if it seems that I'm arguing points you already agree with -- there are others on the thread/forum that are arguing for other things or have ambiguous-to-me viewpoints.
Your implementation looks really awesome, thank you for pursuing it!!
Indeed, I confused the two of them, thank you for clarifying!