[swift-evolution] [Concurrency] async/await + actors
Brent Royal-Gordon
brent at architechies.com
Sat Aug 19 00:29:15 CDT 2017
> On Aug 18, 2017, at 12:35 PM, Chris Lattner <clattner at nondot.org> wrote:
>
>> (Also, I notice that a fire-and-forget message can be thought of as an `async` method returning `Never`, even though the computation *does* terminate eventually. I'm not sure how to handle that, though)
>
> Yeah, I think that actor methods deserve a bit of magic:
>
> - Their bodies should be implicitly async, so they can call async methods without blocking their current queue or have to use beginAsync.
> - However, if they are void “fire and forget” messages, I think the caller side should *not* have to use await on them, since enqueuing the message will not block.
I think we need to be a little careful here—the mere fact that a message returns `Void` doesn't mean the caller shouldn't wait until it's done to continue. For instance:
listActor.delete(at: index) // Void, so it doesn't wait
let count = await listActor.getCount() // But we want the count *after* the deletion!
Perhaps we should depend on the caller to use a future (or a `beginAsync(_:)` call) when they want to fire-and-forget? And yet sometimes a message truly *can't* tell you when it's finished, and we don't want APIs to over-promise on when they tell you they're done. I don't know.
> I agree. That is one reason that I think it is important for it to have a (non-defaulted) protocol requirement. Requiring someone to implement some code is a good way to get them to think about the operation… at least a little bit.
I wondered if that might have been your reasoning.
> That said, the design does not try to *guarantee* memory safety, so there will always be an opportunity for error.
True, but I think we could mitigate that by giving this protocol a relatively narrow purpose. If we eventually build three different features on `ValueSemantical`, we don't want all three of those features to break when someone abuses the protocol to gain access to actors.
>> I also worry that the type behavior of a protocol is a bad fit for `ValueSemantical`. Retroactive conformance to `ValueSemantical` is almost certain to be an unprincipled hack; subclasses can very easily lose the value-semantic behavior of their superclasses, but almost certainly can't have value semantics unless their superclasses do. And yet having `ValueSemantical` conformance somehow be uninherited would destroy Liskov substitutability.
>
> Indeed. See NSArray vs NSMutableArray.
>
> OTOH, I tend to think that retroactive conformance is really a good thing, particularly in the transition period where you’d be dealing with other people’s packages who haven’t adopted the model. You may be adopting it for their structs afterall.
>
> An alternate approach would be to just say “no, you can’t do that. If you want to work around someone else’s problem, define a wrapper struct and mark it as ValueSemantical”. That design could also work.
Yeah, I think wrapper structs are a workable alternative to retroactive conformance.
What I basically envision (if we want to go with a general `ValueSemantical`-type solution) is that, rather than being a protocol, we would have a `value` keyword that went before the `enum`, `struct`, `class`, or `protocol` keyword. (This is somewhat similar to the proposed `moveonly` keyword.) It would not be valid before `extension`, except perhaps on a conditional extension that only applied when a generic or associated type was `value`, so retroactive conformance wouldn't really be possible. You could also use `value` in a generic constraint list just as you can use `class` there.
I'm not totally sure how to reconcile this with mutable subclasses, but I have a very vague sense it might be possible if `value` required some kind of *non*-inheritable initializer, and passing to a `value`-constrained parameter implicitly passed the value through that initializer. That is, if you had:
// As imported--in reality this would be an NS_SWIFT_VALUE_TYPE annotation on the Objective-C definition
value class NSArray: NSObject {
init(_ array: NSArray) { self = array.copy() as! NSArray }
}
Then Swift would implicitly add some code to an actor method like this:
actor Foo {
actor func bar(_ array: NSArray) {
let array = NSArray(array) // Note that this is always `NSArray`, not the dynamic subclass of it
}
}
Since Swift would always rely on the static (compile-time) type to decide which initializer to use, I *think* having `value` be non-inheritable wouldn't be a problem here.
> It would be a perfectly valid design approach to implement actors as a framework or design pattern instead of as a first class language feature. You’d end up with something very close to Akka, which has provides a lot of the high level abstractions, but doesn’t nudge coders to do the right thing w.r.t. shared mutable state enough (IMO).
I agree that the language should nudge people into doing the right thing; I'm just not sure it shouldn't do the same for *all* async calls. But that's the next topic.
>> However, this would move the design of the magic protocol forward in the schedule, and might delay the deployment of async/await. If we *want* these restrictions on all async calls, that might be worth it, but if not, that's a problem.
>
> I’m not sure it make sense either given the extensive completion handler based APIs, which take lots of non value type parameters.
Ah, interesting. For some reason I wasn't thinking that return values would be restricted like parameters, but I guess a return value is just a parameter to the continuation.
I guess what I'd say to that is:
1. I suspect that most completion handlers *do* take types with value semantics, even if they're classes.
2. I suspect that most completion handlers which *do* take non-value types are transferred, not shared, between the actors. If the ownership system allowed us to express that, we could carve out an exception for it.
3. As I've said, I also think there should be a way to disable the safety rules in other situations. This could be used in exceptional cases.
But are these three escape valves enough to make safe-types-only the default on all `async` calls? Maybe not.
>> To that end, I think failure handlers are the right approach. I also think we should make it clear that, once a failure handler is called, there is no saving the process—it is *going* to crash eventually. Maybe failure handlers are `Never`-returning functions, or maybe we simply make it clear that we're going to call `fatalError` after the failure handler runs, but in either case, a failure handler is a point of no return.
>>
>> (In theory, a failure handler could keep things going by pulling some ridiculous shenanigans, like re-entering the runloop. We could try to prevent that with a time limit on failure handlers, but that seems like overengineering.)
>>
>> I have a few points of confusion about failure handlers, though:
>>
>> 1. Who sets up a failure handler? The actor that might fail, or the actor which owns that actor?
>
> I imagine it being something set up by the actor’s init method. That way the actor failure behavior is part of the contract the actor provides. Parameters to the init can be used by clients to customize that behavior.
Okay, so you imagine something vaguely like this (using a strawman syntax):
actor WebSupervisor {
var workers: [WebWorker] = []
func addWorker() -> WebWorker {
let worker = WebWorker(supervisor: self)
workers.append(worker)
return worker
}
actor func restart(afterFailureIn failedWorker: WebWorker) {
stopListening()
launchNewProcess()
for worker in workers where worker !== failedWorker {
await worker.stop()
}
}
…
}
actor WebWorker {
actor init(supervisor: WebSupervisor) {
…
beforeFatalError { _self in
await _self.supervisor.restart(afterFailureIn: self)
}
}
…
}
I was thinking about something where `WebSupervisor.addWorker()` would register itself to be notified if the `WebResponder` crashed, but this way might be better.
--
Brent Royal-Gordon
Architechies
More information about the swift-evolution
mailing list