Proposal Questions
- In "Actor Reentrancy" it's stated:
Generally speaking, the easiest way to avoid breaking invariants across an
await
is to encapsulate state updates in synchronous actor functions. Effectively, synchronous code in an actor provides a critical section, whereas anawait
interrupts a critical section. For our example above, we could effect this change by separating "opinion formation" from "telling a friend your opinion". Indeed, telling your friend your opinion might reasonably cause you to change your opinion!
Could an example be provided of "encapsulate[ing] state updates in synchronous actor function"? How is this any different than the already synchronous state updates taking place in the example?
- In "Deadlocks with non-reentrant actors":
Deadlocked actors would be sitting around as inactive zombies forever. Some runtimes solve deadlocks like this by making every single actor call have a timeout (such timeouts are already useful for distributed actor systems). This would mean that each
await
could potentiallythrow
, and that either timeouts or deadlock detection would have to always be enabled. We feel this would be prohibitively expensive, because we envision actors being used in the vast majority of concurrent Swift applications. It would also muddy the waters with respect to cancellation, which is intentionally designed to be explicit and cooperative. Therefore, we feel that the approach of automatically cancelling on deadlocks does not fit well with the direction of Swift Concurrency.
Should we take this to mean Swift Concurrency will have no solution for deadlock detection or mitigation? Are there any solutions for developers to detect or otherwise avoid them? Or will this be the domain of tools outside the language like the Thread Sanitizer?
- In "Unnecessary blocking with non-reentrant actors":
With a reentrant actor, multiple clients can fetch images independently, so that (say) they can all be at different stages of downloading and decoding an image. The serialized execution of partial tasks on the actor ensures that the cache itself can never get corrupted. At worst, two clients might ask for the same image URL at the same time, in which there will be some redundant work.
As someone with a keen interest in adopting Swift Concurrency for networking, I can tell you it's key to have a solution for avoiding redundant work like network requests and image processing. This is especially true for libraries like AlamofireImage which offer extensible image processing pipelines for downloaded images where it's important to avoid reprocessing large images through CoreImage and other systems. Does (or will, in other proposals) Swift Concurrency offer any solution for avoiding these issues? (This may also relate to my later questions.)
General Design Questions
Actors' ability to verify thread safety will be extremely valuable to the community. Combined with async
/ await
, it should greatly simplify the encapsulation of shared mutable state. I look forward () to rewriting Alamofire yet again with this vastly simpler model. I spent quite a lot of time in Alamofire 5 getting synchronous properties and methods to play well with internally asynchronous actions while maintaining safe mutation and serial execution on shared DispatchQueue
s.
That said, I'm extremely wary of the async
requirement for all outside actor
access, especially without async
properties or subscript
s (yes, it's being pitched). This both makes actor
s less useful in non-async
contexts and, perhaps more importantly, prevents developers from designing their ideal APIs. That is, it prevents them from offering synchronous APIs and properties even if they're perfectly safe.
More concretely, Alamofire's current design seems like an ideal application of actor
s. Requests are modeled as subclasses of the Request
classes, largely mirroring the hierarchy of URLSessionTask
(Request
-> DataRequest
-> UploadRequest
, Request
-> DownloadRequest
, Request
-> DataStreamRequest
, and (eventually) Request
-> WebSocketRequest
). Request
encapsulates all of the shared state, including mutable state, as well as internal API common to all Request
s. Each subclass maintains whatever additional mutable state is necessary, and all mutable state that has public API is protected by a wrapper using os_unfair_lock
. State that isn't expected to be publicly accessed is unprotected but safe access is ensured by the fact that all intercommunicating classes share an underlying serial DispatchQueue
. So Request
s communicate back to their originating Session
, and vice versa, without locks.
Part of the API of Request
I'm most concerned about are the various synchronous APIs for state changes and access to the various bits of state itself. For instance, like URLSessionTask
, Request
exposes resume()
, suspend()
, and cancel()
methods which operate synchronously. Actions like adding a response handler are also synchronous and return the Request
instance, allowing users to attach multiple handlers. Additionally, various bits of the Request
's state is available publicly, like all of the URLRequest
s or URLSessionTask
s its performed. So while the actions makes sense asynchronously, access to state properties really don't. Of course we could just expose requests()
instead of requests
, and maybe we'll get async
read-only properties, but this seems to be exposing implementation details and deviates from typical Swift design. So it leads to a basic question.
How pervasive are async contexts intended to be? That is, what is the UX impact of having to make every method (and maybe property) async
when using actors? Will there be a default, global actor that everything starts on and allows global awaiting?
Relatedly, a large amount of the API I've mentioned doesn't need to be async by definition and would only have the requirement due to being an actor
. This seems to expose unnecessary underlying implementation details, just like it would if I exposed the various state properties or handler methods using completion handlers rather than making them externally synchronous while encapsulating internal locking or async work. So it certainly seems like this proposal will make a lot of APIs async
when they don't otherwise need to be. That is, they could be safely exposed synchronously if only it was made possible. Will such capabilities be exposed for actor
s? If not, while I disagree with those who say try
or await
are overused, it will certainly be the case that async
will suddenly become far more widespread than seems necessary, leading to a proliferation of await
even for simple APIs.
Finally, there was nothing in the proposal about the intended performance or other system impact of actor
s. Users often want to enqueue perhaps thousands of asynchronous actions, so what are the limitations of actor
s in that scenario? In Alamofire 5 we took great pains to move away from the previous "one DispatchQueue
per Request
" model to a shared, serial DispatchQueue
that underlies all Request
s made by the same Session
, eliminating one major bottleneck for users wanting to enqueue thousands of network requests at the same time (it's still not a good idea). It would be a shame for actor
s to reintroduce such limitations.