[Pitch #2] Actors

I don't agree. The await signals a potential suspension point: your code may be suspended, and any kind of global state (unprotected globals anywhere, clocks/timers, etc.) might change out from under you before you resume. The "reentrancy" discussion is about whether your actor state might also change out from under you before you resume.

It's reentrant. I've clarified the specification and added an example.

Fixed, thanks!

I've switched it to use @reentrant(never) and dropped a stale reference to a since-removed example, thanks!

As noted above, I've added an example and clarified the wording a bit.

I've added a simple example and reworded this a bit.

Ah, I see how you came to this conclusion, and it is somewhat of a side effect of us splitting global actors out of the proposal. With global actors, any declaration anywhere can be actor-isolated with the appropriate annotation. For example,

@MainActor var dataOnlyAvailableOnTheMainActor: String

@MainActor
extension MyViewController {
  func f() { ... }                                    // actor-isolated to the global actor MainActor
  @actorIndependent func g() { ... }  // actor-independent
}

On re-reading, I noticed that a bit of the discussion on @actorIndependent describes rules that are too general and don't make sense without also considering global actors. At a minimum, we need to get a pitch out there for global actors. I don't have a strong opinion on whether we should narrow the definition of @actorIndependent in this proposal down to the definition that makes sense in this proposal, then widen the definition again in global actors... but it seems like that will make the proposals harder to understand.

This is an interesting point. Actor isolation is certainly an important part of the declaration, with far more semantic impact than one would normally expect from an attribute. I like this suggestion. The only wrinkle I see is the use of @actorIndependent on closures, which isn't quite as amenable to contextual keywords. That said, we have the contextual keyword async in closures already, so we can probably handle this.

Hmm, the rules are spelled out for overrides and conformance, but I suppose we could use more examples here.

Actor isolation isn't part of the ABI, so it's not ABI-breaking to change actor isolation... but it's certainly source-breaking and could lead to unfortunate runtime effects if (say) your client was compiled when your function was actor-independent and now it is actor-isolated.

Ah, we meant "on a type", as in my example above.

Yes, it's reasonable to want to specifically state that something is intended to be isolated. Closures could probably benefit from this as well. (Again, I get slightly worried about the parsing of something like { isolated in ... }, because changing that meaning would be source breaking)

Thank you!

Doug

3 Likes

It's in the domain of tools, like (for example) finding reference cycles.

Sure, you can build it out of the image downloader example + detached tasks from structured concurrency. Instead of the cache being this:

var cache: [URL: Image] = [:]

turn it into something that either stores an image or a Task.Handle that creates the image:

enum DelayedImage {
  case image(Image)
  case handle(Task.Handle<Image, Never>)
}

var cache: [URL: DelayedImage] = [:]

Then in your getImage(), you check which case you're in: if you're the first access to that URL, you create a detached task to do the work and stash it in the cache. Otherwise, you can wait until the task completes. Here's a sketch:

func getImage(_ url: URL) async -> Image {
    if let cachedImage = cache[url] {
      switch cachedImage {
        case .image(let image): // already retrieved
          return image

        case .handle(let handle): // someone else is doing the work now
          return await handle.get()
      }
    }

    // okay, we need to do the work ourselves. Spawn off a task to do it
    let handle = Task.runDetached {
      let data = await self.download(url)
      let image = await Image(decoding: data)
      return image
    })

    // Let everyone else know how to get the image
    cache[url] = .handle(handle)

    // Wait for our task to complete
    let image = await handle.get()

    // Update the cache before we return
    cache[url] = .image(image)
    return image
}

I don't really know how to answer this question: code that involves actors will need to use async. One can spawn off detached tasks from synchronous code to perform the asynchronous interaction with the actor, much like one would put code to interoperate with asynchronous completion-handler methods inside the completion closure.

In the upcoming "global actors" proposal, there will be a @MainActor that is an actor representing the main thread, to cope with tasks that must run on the main thread (hello, UI libraries). But it's opt-in, not a default for non-isolated code.

To be clear, the APIs themselves can be synchronous, but access to them from outside the actor will require an asynchronous call. In the defined model, it is safe to access immutable actor instance data (e.g., a let), but not any mutable data. You can suppress the checking with @actorIndependent(unsafe), which could also be used (say) if you wanted to do your own locking inside an actor for some of the state.

We've generally avoided trying to make performance claims in a proposal (ever), because they're not something readily verifiable and would tend toward being vacuous. To be clear, an actor instance does not hold its own DispatchQueue: it has a light-weight queue of partial asynchronous tasks that are cooperatively scheduled. We expect an outer (concurrent) DispatchQueue to provide the threads themselves, and the actor runtime will select an actor that has pending tasks to run. Feel free to check out the actor runtime implementation.

Doug

5 Likes

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.

Doug

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.

Yes, I'm awaiting convergence.

Doug

5 Likes

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.

Doug

3 Likes

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."

Doug

2 Likes

Makes sense, thanks.

Makes sense, thanks for documenting it!

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 :slight_smile:

I'm talking about all the legacy methods on NSObject class:

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.

-Chris

4 Likes

Not sure how a global actor would help here

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
	}
}
1 Like

Looks like an infinite recursion to me. It doesn't block other programs from using Deadlock, though (if it's reentrant).


IMO, I think that non-reentrant is a very dangerous tool. Maybe we only need @reentrant(task)?

The task ends up waiting for itself in the second invocation of getValue. (This should at least be dynamically detectable though)

1 Like

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.

Are you referring to this quote?

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).

1 Like

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:

Please correct me if I've misunderstood anything!

Just two quick notes:

  1. An argument for non-reentrancy is when data corruption is worse than crashing/deadlocking.
  2. Having a separate spelling of await for non-reentrancy would be awesome!

I would perhaps call this "deadsuspend" :stuck_out_tongue: It's more like your order is lost :slight_smile: Like you are sending requests to the wrong mailbox.

It's a very interesting finding, though :+1: And I guess it could easily result in making a big part of the program not progress anymore.

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.

Doug

3 Likes

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!

-Chris

Hey folks, in case you're following along here, I've posted a third revision of the actors proposal in a separate thread: [Pitch #3] Actors

1 Like