This seems like a dumb or obvious question: Actors are suppose to only allow one caller to mutate its state at any time, and so it serialises all access to itself. But if a function, eg update() async that it has calls another async function inside its body, and suspends its execution, does the actor become free to execute another call to same function?
Actor {
func update() async {
let result = await tryCalling() #special case when an actor method suspends to wait for another call to finish
}
}
TaskA {
await update()
}
TaskB {
await update()
}
Assuming task a and b schedules concurrently, would the Actor of update still wait for the first update call that arrived to finish before running the second ?
In general are actors always serialising if you dont mark the function as nonisolated? eg even for methods that have no state like a function that all it does is calculate the sum of its inputs.
Yes, at each await in your code, the actor may be free to run other work, including other calls to the same function. The benefit of this is that it avoids deadlocks and allows more concurrency. The downside is that your code needs to be written with the assumption that actor state may be different than it was before the await.
Yes. This can be useful if you want to limit the concurrency of a function, for example if it's an algorithm that doesn't scale well, or if it allocates a lot of memory. But mostly it just makes the behavior consistent; otherwise adding an innocent looking access to actor state in a method would cause it to suddenly behave differently.
It's worth noting that this is a feature of the async/await programming model in general - it looks like synchronous code, but arbitrary amounts of time can pass across an await. So you really want to make function-local copies of mutable values which need to stay stable across those points. This is the same across languages which implement async/await.
When it comes to actors, it just takes a bit of getting used to because we're used to thinking of self's instance data as being as stable as a local variable (because we're used to thinking of functions which don't suspend).
I think that just getting in to the async/await model is enough to make actor behaviour feel intuitive. That doesn't mean it will necessarily be easy to adapt your design to it, though.
Are we actually saying the actor would schedule another piece of code to execute while an existing function is still in progress? Wouldnt that cause potential concurrency problems that actors are supposed to solve? ie data races?
Sorry like I say, this question I am asking could be stupid.
Another reason Im asking is that currently in my work I am having a crash related to concurrent reads and once changing the class to Actor, the crash stopped. (the code structure is exactly the same as my example given)
Actors only solve the lowest-level data races, like how calling x += 1 on two different threads at the same time sometimes increments it by 1 total and sometimes by 2.
The ability to call an actor again while one of its methods is currently suspended is called reentrancy. People have asked multiple times for non-reentrant actors, but they haven't been added yet, at least partially due to concerns about deadlock (e.g. what if the method you're awaiting itself calls an actor method?).
Actors do solve data races — when state can be mutated from different async contexts. It does this so that at any given time there is only one operation that is executed on the actor.
await and the kind of race condition it introduces to the actor methods is the reentrancy issue, when single method can be called across suspension points several times.
The catch is that at await execution on the actor (more likely) is not happening and some other party is doing the work. Which allows the same method to be called again, because actor itself doesn’t run any work, therefore it’s state cannot mutate, which makes it is safe to do such thing.
Reentrancy also solves another kind of problems — deadlock. If actors weren’t reentrant, they could cause one relatively easy.
At the same time, state can change around suspension points, making it another issue that requires attention from the developer. It’s tough question which problem is worse, but since Swift Concurrency is about making progress always, the default reentrancy fits nicely.
No such thing as a stupid question :) it's a reasonable objection, just not one anyone has come up with a great answer to. "Engineering is the science of tradeoffs" as they say.
My thinking on this is that it boils down to being a local vs global knowledge problem:
Dropping the actor's internal lock when it does async calls prevents needing to know how the rest of the program works ("does anything this calls call anything which [arbitrarily long chain of calls crossing arbitrarily much of your codebase] calls back into this actor?"), at the cost of invalidating some of your local knowledge about that actor ("is the state I'm accessing still the same as it was?".
Local knowledge is somewhat easier to recover (e.g. you can store a piece of state you care about in a local variable so you can check if the new state is the same) and scales better (it only involves one actor worth of code instead of the transitive closure of everything it calls), so this is a reasonable choice to make.
…but it's still a tradeoff; things would be easier if we didn't need to choose. And like any tradeoff, reasonable people can disagree about what the right choice is, especially situationally.
I will read everyone's posts soon, but for now, are we saying my observation that the use of actors helping to avoid concurrency related crashes in my app is purely coincidental.? Basically I just changed my class to Actor, and the function is the same as the update one i described
The kind of issues that arise with actors reentrancy are of different kind, proposal describes it in detail, but might require few takes: SE-0306: Reentrancy section - it provides an example with DecisionMaker actor. In other words, you need to check if invariant of the state that important to the method flow still holds after a suspension point.
The way I like to think about Swift safety — both memory safety and data-race safety — is that it allows you to debug problems at the Swift layer.
In an unsafe language you regularly find yourself having to drop down a layer to debug a problem. For example, if your C program fails because a variable has the wrong value, it could be because some of your code modified it incorrectly, but it could also be because some completely unrelated code scribbled over that memory. And if it’s the latter, you end up having to debug it at the assembly layer. In Swift, however, memory safety means that you can investigate such bugs exclusively in Swift.
The same applies to Swift concurrency. Its data-race safety doesn’t prevent all concurrency bugs — actor reentrancy being an obvious example of that — but it lets you reason about those bugs in Swift.
Now, this isn’t completely true yet, because our Swift programs call a lot of code that’s written in unsafe languages, but the long-term direction is clear.
I look forward to the day when my ability to debug gnarly problems at the assembly language level is considered to be a quaint quirk of history (-:
Actors are a tool which can be used to share a piece of mutable state across concurrency domains. That's not something you will need all the time, and it may be preferable to design your system to avoid shared mutable state entirely if you can, or to make sure it is only used from one concurrency domain (e.g. by marking it with a global actor such as @MainActor).
But let's say you do need to share state across concurrency domains. A download cache is a good, straightforward example of that - network requests can be happening on any thread, but they should all be looking at the same, single cache. Actors are not the only way to implement this kind of shared state. For example, we recently introduced Mutex<T> as another way to serialise accesses to shared mutable state, and that may be more appropriate in some situations.
Our advice for using each of them is similar: avoid doing expensive work while you have exclusive control of the state. Don't be doing (say) image processing on your actor's executor; treat actor member functions more like the critical section of a Mutex. For mutexes, we don't allow suspending in the critical section at all, and for actors, they allow other jobs to proceed on the actor even while another job is suspended (reentrancy).
If you want something to serialise a piece of state across a series of very long operations, it may be better to use some kind of custom Queue type.
These are the best ways to protect your shared mutable state, because we can express the concurrency-related aspects of how they work in Swift, which the compiler can use to verify that your code is free of low-level data races. If you're doing something custom to serialise accesses to your state, and you are absolutely sure it is already free of races, you can also mark your existing class as @unchecked Sendable. You don't necessarily need to use an actor.
If you aren't doing anything currently to protect your shared mutable state, and it is being used simultaneously across concurrency domains, then...
actors helping to avoid concurrency related crashes in my app is purely coincidental.?