The ImageDownloader example in the proposal gives me similar vibes. The text says:
At worst, two clients might ask for the same image URL at the same time, in which there will be some redundant work.
To me that is a pretty fundamental issue with the image downloader example. If you have 10 identical images on a page and you ask for them upon loading the page, you don't want to issue 10 requests. This downloader actor is concurrency-safe and non-blocking, but not well-behaved.
The proposal says this is better than if the actor was non-reentrant (because then it'd serialize each request). I'd argue neither the reentrant or non-reentrant versions are good: they each choose a different and unnecessary tradeoff.
A proper implementation would store future-like task handles in that cache so you're able to await on an existing request already in flight.
I don't know if that means actors are deficient. It might just mean overenthusiastic people will try to implement things using actors (like that downloader in the proposal) only to end up having to use other means because it falls short of their expectations. It sounds quite possible to me that we're overhyping actors and their capabilities right now. They seem quite limited to me, which is not necessarily a bad thing as long as people realize that.
In a (model, view, controller) scenario; wouldnât Request be more of a model than a controller?
My high level understanding is that Actors are more of the controller than the model. Session fits perfectly as an Actor.
I do think you have some good points about what is the high level scenarios where Actors are meant to help.
I would love to see an example from the authors for converting an mvc app to use Actors. I would imagine that View is not going to be an actor but ViewController would be an Actor.
Interesting you have that impression, the first example the pitch gives is of model objects (a BankAccount) as actors.
Can anyone articulate the incompatibility between @Jon_Shierâs type or Future and actor concepts?
The pitch seems to give a definition of actors that seems compatible with those types but we have some of the issues @Jon_Shier raised and @John_McCall saying actors donât suit Futures. What opinions about app architecture are actors subtly imposing that âa bag of state is held within a concurrency domain and then define multiple operations that act upon itâ does not give?
My main point was that actors shouldn't expose their internal details to consumers. That is, consumers of actors shouldn't have to care about how to call synchronous looking methods because they internally touch the actor's mutable state. Unless the author of the actor wants async APIs, they shouldn't be forced to have them just to take advantage of the protection actors offer.
No, as that isn't real synchrony. As soon as this method completes, isCancelled should be true. That's important both for the consumer as well as internal work already enqueued which needs to check the value in order to stop processing.
Yes, something to that affect. Ideally this would just be automatic with sync methods on actors, but having to internally demarcate such sections would also work. That's what I have to do today with the lock.
AIUI, runDetached isn't just asynchronous, it can also run in parallel on another queue. This violates the safety I need. Something similar that simply asynchronous on the actor's internal queue would work fine, as that's essentially the pattern I have now.
I've also seen effect-ful properties pitched, which would be better but doesn't actually solve my issue. As I understand it, a nonisolated property wouldn't have access to the internal state I need without providing my own lock. At that point I've lost much of the value actors provide. And my overall point is that I shouldn't have to offer an async API surface just because I'm an actor, properties included.
Yes, that example isn't very realistic. It would be better if the proposal was updated to use the Task.Handle storage example demonstrated to me in an earlier pitch thread.
If the model can't be demonstrated with realistic examples, then I'd consider it deficient. Protection of mutable state that's only useful in certain cases that aren't described in the proposal would seem to require further definition at least.
Again, no. In this case, a model would be something like URLRequest or any of the other various model's Alamofire uses internal to describe the Request's settings. As I've describe, Requests encapsulate several long running tasks (most notably, the actual URLSessionTasks being performed) with an eventual asynchronous completion. That is far more controller than model. Session will also benefit from automatic thread-safety but actually performs far less work on different queues.
That's actually use case I don't think is well suited to actors, as you really shouldn't be mutating view controller state from multiple queues. However, I do think integrating how UIViewController will change when Swift concurrency lands (if at all) would be very illuminating and should be part of a proposal or forum thread at some point.
runDetached creates a new task that's independent of the current task (if there is one). It doesn't override isolation rules; if you use that task to, say, call an actor function, that part of the task will of course run on the actor.
Interesting. So the body of the closure could (would?) run in parallel with other executors, but my calls to the actor itself are executed on that context? I think that would replicate the queue.async behavior I need. I still think it would be better to have actor API dedicated to enqueuing things internally without the need to create a separate Task that just calls back to the actor, but at least the behavior's there.
Further to this, if there is some common patterns the application of actors to which are not suitable, a section in the proposal should outline those (ideally, it should explain why). This may help explain how actors should be applied, just like studying fallacies helps explain clear thinking.
For example, if the concept of futures do not appropriately match with the concept of actors, the proposal could say, âa future should not be considered an actor because it represents a single value, where an actor is a box of mutable state which changes between values. Implementing futures as actors results in awkward, counter intuitive API designâ (this isnât a particularly good example for me because I donât see how the implementation of a future whose state is modelling the existence of a value is disjoint from the concept of an actor).
It's the fundamental shape of an actor that its API is async. Especially for distributed actors. But the current proposal does allow for some synchronous API (at least last I checked).
So what I'm proposing is to have a synchronous cancel() that will update the state protected by the lock, so isCancelled is true, and then use runDetached() to notify listeners, update the non-sync state etc. So cancel() will be sync, exit without any suspension points, and isCancelled will be true immediately after.
So as @John_McCall already mentioned this does not violate the actor's safety.
Swift Concurrency is of course fundamentally different from using threads or libdispatch, although it looks almost the same. What previously would block (bad) will now cooperatively allow progression of other work (good), and where you would previously need to take care to specify the thread or queue to run something on, the actor now takes care of that and you can use it from any thread. And where you would previously need to be careful with how many threads you spawned, this is now automatically limited to the number of CPU cores.
So I guess you could say that queue.sync() is not needed anymore, and queue.async() is basically just runDetached().
But the most fundamental difference is the cooperative multitasking. Previously we would allocate a separate thread or queue for each distinct piece of work, pretending that it would run in parallel without affecting other work. But of course that doesn't scale; if you have enough work you need the scheduler to force "suspension points" in random places, and the "run in parallel" becomes an illusion. What's worse is that the system really has no idea of the structure of your work, so if a libdispatch worker thread is blocked it needs to spawn more worker threads to ensure the global progression of work.
As I see it, the achilles heel of cooperative multitasking is tasks not cooperating. I.e. long-running tasks with no suspension points, or tasks blocking using "old school" mutexes and the like.
So please move as much state as possible out of MutableState, and please only do the minimal required work while holding the mutex
Even if that's true, that doesn't mean it's a good direction for Swift. I'd rather the feature fit the language than the language adapt to suit the feature. Given that Swift has existing async patterns in common use it seems better to adopt those patterns than build completely new ones. It certainly seems to me that a hybrid approach gives us the best of both worlds: compiler checked safety with an easy to use model. (And the current proposal has removed the isolation keywords, so there's no isolation control officially.)
I'm not sure why you wrote those paragraphs, as Alamofire has already adjusted to the limitations of GCD in version 5. A single serial queue is now shared between the Session and all Requests, will all mutations that aren't synchronous or from outside the framework being done on that queue. MutableState is locked to prevent reentrancy in certain scenarios and to ensure users see state changes immediately after calling particular APIs. Internally this isn't fundamentally different from the actor model proposed, I just don't expose the async nature of my state updates to my users.
Sorry, this was more of a general warning for others going to use a mutex in an actor, which is usually not the best design. I'm sure you have been shaving your MutableState down to the bare minimum
In Alamofire's current design, MutableState is fairly large, as it encapsulates all of the state that has public API allowing it to mutate from multiple queues (all user facing API). Accesses are engineered to ensure minimal work is done while holding the lock (a wrapper around os_unfair_lock), which is why the lifetime methods are actually in underlyQueue.async calls in the first place. This ensures the state updates are atomic and block other mutating methods from issuing their lifetime methods until the first mutations are done. There's really no other option here.
I was hoping actors would solve a lot of this complexity but without the ability to offer synchronous API that automatically integrates with the actor safety mechanisms, it seems I have no choice but to continue using a lock internally. That greatly reduces the value of actors altogether, which is why I think they need synchronous capabilities in addition to the async ones proposed. Additionally, when I raised this point in one of the first pitch threads, a lock was suggested a way to manage my own state outside of the isolation model. Whether that recommendation has changed, I don't know. We'll find out in the specific isolation API pitch.