It will take about 10 minutes to finish reading this article.
1. The Birth of Actor
class allows creating types with mutable properties and sharing them throughout a program. However, classes have problems in concurrent programming, requiring developers to manually implement error-prone synchronization mechanisms to avoid data races.
Our goal is to allow global sharing of mutable state while maintaining static detection of data contention and other concurrency issues. And the actor model can be a good solution to this problem. Each actor ensures the independence of its data through data isolation, thus ensuring that only one thread can access that data at a time.
Note:
Actor model is a concurrent computing programming model, originally proposed by computer scientist Carl Hewitt and others in 1973, it is not owned by a certain language or framework, and can be used in almost any programming language. It ensures data security through data isolation. Its principle maintains a serial queue (mailbox) inside, and all external calls involving data security are executed serially.
See: https://en.wikipedia.org/wiki/Actor_model
Swift introduced Actor as part of the Swift concurrency family in version 5.5. the actor model guarantees the same race conditions and memory safety as structured concurrency, while providing a Swift abstraction that is familiar to developers, and implementing features that are available in other explicitly declared types.
2. Concept of Actor
In Proposition 0306, Actor is a reference type, and except for the lack of inheritance support, actor is very similar to class: it can adhere to specified protocols, support extensions, etc.
1 | actor MyActor { |
In order to ensure data security under concurrency, the Actor internally executes external accesses serially. Therefore, such accesses are asynchronous, which means that they do not return a result immediately, but are queued up and executed sequentially. Therefore, such accesses need to be executed via await.
3. Actor isolation
Actor isolation is a way to protect its mutable state by isolating the actor instance as a unit (boundary) from the outside world and severely restricting cross-boundary access, so that only direct access to its stored instance properties is allowed on self.
1 | objectA.title = "New title!" |
The above code will report an error:
1 | Actor-isolated property title can not be mutated from the main actor |
4. Nonisolated
Nonisolated, as the name implies, means non-isolated. the mailbox serial access mechanism inside the actor is bound to have some performance loss, and methods and computation attributes inside the actor do not always cause Data races. therefore, the “Improved control over actor isolation” in SE0313 provides an explicit way for clients to freely synchronize access to immutable actor states with the keyword nonisolated.
1 | extension MyActor { |
This allows you to call the following code directly, and since you don’t need to go through the asynchronous serial mechanism, you don’t need to add await:
1 | let num = objectA.changeIntToSting(num: 1002) |
Note:
It is not possible to access the isolated state in the nonisolated method, for example:
1 | nonisolated func changeIntToSting(num: Int) -> String { |
Remove nonisolated and turn it into an internal method of the actor, where methods and properties can be accessed directly.
1 | extension MyActor { |
5. Actor Reentrancy
Actor Reentrancy is the context of entering the same actor multiple times in the same thread.Actor-isolated methods are reentrant, which means that the same actor’s method can be called multiple times in the same thread without blocking or waiting.
This reentrancy can improve performance in some cases because it allows a task executing an actor-isolated method to not wait for the previous call to complete if it needs to call the method again. This flexibility can reduce the overhead of thread switching and thus improve performance.
Specific attention should be paid to:
(1) Actor-isolated methods may have pause points within them when explicitly declared as asynchronous methods;
(2) When an Actor-isolated method is hung due to a pause point, the method is reentrant, i.e., it can be accessed again before the previous hang is resumed;
1 | actor MyActor { |
The call is as follows:
1 | let myActor = MyActor(title: "Starting title!", num: 1001, capacity: 9000) |
The results of the implementation are as follows:
1 | Comsume succeeded, capacity = 4000 |
Where did it go wrong?
Generally, the two-step operation of “check-modify” should not cross the await suspension point. They belong to an “atomic operation”, and the await suspension point may “cut” them. How to solve this problem? , you can check it again before modifying:
1 | func consumeCapacity(consume: Int) async throws -> Int { |
The results are as follows:
1 | Consume succeeded, capacity = 4000 |
So, this issue needs to be taken care of.
6. globalActor and MainActor
6.1 globalActor
An actor is data-protected at the boundaries of its instances. globalActor is designed to be thread-safe for global variables, static attributes, and cross-type/cross-instance. A global actor is a type that has the @globalActor attribute and contains a static attribute called shared that provides a shared instance of an actor. Example:
1 |
|
1 |
|
This makes the global variable globalValue thread-safe.
1 | Task { |
6.2 MainActor
MainActor is a special case of globalActor. MainActor is a global actor that describes the main thread, and the methods, properties, etc. modified by MainActor will be executed on the main thread.
1 |
|
We often use ‘MainActor.run’ to execute a piece of code on the main thread:
1 | await MainActor.run { |
Reference
[1] https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md
[2] https://juejin.cn/post/7076738494869012494
[3] https://zhuanlan.zhihu.com/p/86460724