Swift Concurrency Series 05|The difference between Actor and traditional thread-safe writing methods
Actors are not "Swift locks". What they really change is how shared state is organized.
When you hear Actor for the first time, you will subconsciously understand it as “a thread safety tool provided by Swift”.
This understanding is not completely wrong, but if you just stop here, it will be easy to write code that “uses actors, but the structure is still messy”.
Because the most important thing about Actor is that it changes the default idea:
Shared mutable state should not be exposed to everyone first and then find ways to protect it; a more reasonable approach is to isolate it first and then decide how to access it from the outside.
This may sound like a difference in wording, but it’s actually a watershed in concurrency design habits.
1. The most dangerous thing in concurrency is often shared mutable state
Most concurrency bugs start from:
- Two tasks read and write the same cache at the same time
- One task has not been written yet, and another task has already made a judgment based on the old value.
- Multiple pages modify a global status at the same time
- A certain service is responsible for refreshing tokens, consuming tokens, and broadcasting the results.
These problems may appear to come in many forms, but the root causes are often the same: **Shared mutable state has no clear boundaries. **
Once the state boundaries are unclear, any change in task scheduling order may amplify bugs.
2. The problem with traditional thread safety solutions is that they are not centralized enough.
The solutions we commonly used in the past include:
NSLock- serial queue
- read-write lock
- Team agreement that “this object can only be accessed on a certain queue”
None of these solutions are wrong, and many mature systems are still in stable use today. Their real problem is:
**Access rules are easily scattered. **
At first you may still remember:
- This cache must be accessed with a lock
- That dictionary can only be changed in the serial queue
- A certain state can only be read and written in the main thread
But as the number of call points increases, these rules will slowly become distorted. The most annoying thing about concurrency bugs is that it is often no longer possible to ensure that the entire team strictly abides by those scattered rules.
3. The core idea of Actor is “reduce sharing first”
This is the most important point worth repeating.
The traditional idea is more like:
- First have a shared status
- Then think of ways to protect it through locks, queues, and conventions
Actor’s idea is closer:
- This state should not be accessed arbitrarily.
- I put it into the isolation boundary first
- The outside world can only interact with it through the controlled interface
The biggest change this brings is that we are forced to think about status ownership, instead of acquiescing that everyone can directly touch it.
For example, if a token refresh coordinator is just a global object with a bunch of locks, the default mentality is “Everyone is using this state, and I have to protect it.”
If it is Actor, the default mentality will become “This state is managed by it, and others can only ask it for results or send commands.”
This is how design changes.
4. This isolation approach is more suitable for complex projects
Because what complex projects fear most is the proliferation of rules.
Once state access is blocked by Actor, many problems will be exposed earlier:
-Who controls this data?
- Is the external requirement a snapshot, a derived value, or an imperative operation?
- Which operations must have serial semantics and which are just read-only queries
These problems were often pushed back in the old model, because everyone defaulted to “share first, and then lock if there are problems.” Actors, on the other hand, force boundary design earlier.
Therefore, I prefer to think of Actor as a concurrency design tool rather than just a thread safety tool.
5. Where is the best place to place Actor?
Not all objects are worthy of being turned into Actors. It is more suitable for roles that meet the following characteristics at the same time:
- Have shared mutable state
- Will be accessed concurrently by multiple tasks
- Consistency is important
- Access methods need to be centrally coordinated
Typical examples include:
- Image cache coordinator
- Token refresh coordinator
- Download task registration center
- Some kind of global resource accessor
- Message center that requires serial consumption of events
What these objects have in common is that they are state centers that are shared across tasks and prone to concurrency problems.
If it is just a pure function service, or a state object that is only used briefly inside a single page, an Actor may not be needed.
6. What is the essential difference between Actor and “Add a lock to the entire class”?
On the surface, they can all do “multiple tasks without changing the status”. But the engineering experience varies greatly.
When locking an entire class, common questions are:
- The caller can still get a lot of internal details
- The granularity of locks can easily get out of control
- Some methods call other synchronization resources, forming complex nesting.
- Team members may still bypass the rules and directly access status they should not access
Actor, at least at the semantic level, clearly expresses:
- This status is not freely shared
- Cross-isolation access needs to explicitly pass through the asynchronous boundary
- Accessing this state is itself a constrained behavior
In other words, Actors not only “help add protection”, but also make insecure access methods more difficult to write casually.
7. Actor cannot solve anything
This must be made clear. When you first learn this content, you will have the illusion that thread safety will be left to it in the future.
In fact, Actors only solve one type of problem: isolating shared state.
It doesn’t solve:
- Is the business sequence itself reasonable?
- Should old tasks continue after leaving the page?
- Whether the result has expired
- Is the state boundary drawn wrongly?
For example, if the search result cache is made into an actor, but the page still allows old requests to overwrite new keyword results, the bug will still exist. Because the problem here is that “result validity” is not modeled in the first place.
So Actors are important, but they are not a panacea for concurrency.
8. The two most easily misused directions of Actor
1. Try to fit everything into the Actor
Once actors are regarded as “safer as long as they are used” label, they will start to over-package:
- The state that is obviously only used in a single-threaded context is also packaged as an Actor
- The logic that is obviously more suitable for pure function processing is also forced into Actor
- In the end, the entire system is full of asynchronous access boundaries, and the structure becomes heavier.
More Actors is not always better. The key is to place them where shared state is truly needed to be isolated.
2. Treat Actors as “security proof”
Once actors are added to some code, the team will assume that this piece of code is “thread-safe”. But many bugs come from:
- Wrong life cycle design
- Wrong state flow
- Wrong task relationship
Actor can guarantee “Don’t mess with concurrency and touch my internal state”, but it cannot guarantee “the business process is correct.”
9. A more practical judgment question
If you are hesitating whether an object should be designed as an Actor, I suggest you not ask “Will it be thread-unsafe” first, but first ask:
- Does it have important shared mutable state inside it?
- Will this state be accessed by multiple tasks at the same time?
- Do I want the outside world to interact with it only through controlled interfaces?
- If the isolation boundary is not added, will it be easy for the team to misuse it in the future?
If the answer to most of these questions is “yes,” then it’s probably a good fit for the Actor.
10. Conclusion: The real value of Actor is to change shared mutable state from default exposure to default isolation.
To put it in shorter form, I would say:
ActorWhat has really changed is that “shared mutable state is finally no longer exposed to everyone by default.”
The traditional thinking is more like: share first, then protect. The idea of Actor is: isolate first, and then decide how to access.
In complex concurrent systems, this change in thinking is often more important than “one more thread safety tool”.
读完之后,下一步看什么
如果还想继续了解,可以从下面几个方向接着读。