Easy Actor, Async, and Await in Swift Concurrency

ClickUp
Note
AI Status
Last Edit By
Last edited time
Aug 29, 2023 10:58 AM
Metatag
Slug
how-to-use-actor-in-swift
Writer
Published
Published
Date
Aug 2, 2023
Category
Swift
Actor is a new feature introduced in Swift 5.7, aimed at enhancing efficiency and ensuring safe concurrency for developers.
To utilize actors, you create a new type using the actor keyword, similar to declaring a class:
actor UserHealth { }
Before delving deeper into actors, it is essential to understand why Swift introduced this feature and its practical applications.

Why Do You Need Actors?

Prior to the advent of actors, developers had to protect their concurrent code from issues using locks. Locking, a mechanism inherited from the C environment, allowed avoiding simultaneous mutations of the same reference from different threads. This logic occurred at runtime and required manual coding by developers.
Whenever safeguarding data from data races was necessary, developers had to write boilerplate code using NSLock, NSSpinLock, and similar constructs. Unfortunately, a misguided decision in implementing locks could lead to more bugs instead of resolving the problem.
Swift's introduction of actors offers a superior approach to tackle this issue.
Now, the compiler can assist in automating data race protection by checking for it during compile time. This is precisely what actors accomplish.

How to Create an Actor

To create an actor, you use the actor keyword:
actor UserHealth { }
An actor functions much like a class but with certain limitations that we will explore later. For now, you can think of actors as being similar to classes, making them familiar and straightforward to use.

Variables

You can add variables to an actor, just like in a class:
actor UserHealth { var health = 100.0 }

Initializer

Similarly to classes, you may need to include an initializer when you have variables:
actor UserHealth { var health = 100.0 init(health: Double) { self.health = health } }

Methods

You can add methods to an actor:
actor UserHealth { var health = 100.0 func getSick() { health -= 7.5 } func getLongSick() async { health -= 50 } }

Generic

Actors can also be made generic:
actor UserHealth<T> { var problem: T? } let userHealth: UserHealth<Headache> = UserHealth()

Subscript

If a user has a list of health issues, you can add a subscript:
actor UserHealth { var issues: [String] = ["Dizzy"] subscript(_ index: Int) -> String { issues[index] } } userHealth[0] == "Dizzy"
However, actors have some limitations.

Actor Limitations

No Inheritance

Actors do not support inheritance. The following code is invalid:
actor UserHealth { } actor FatherHealth: UserHealth { }
Due to this limitation, you cannot use final, override, or convenience initializers in actors, and you should avoid attempting to use them.

No External Mutability

All mutable state must be modified from inside the actor. The following code is an invalid way to mutate the health state:
let userHealth = UserHealth() userHealth.health += 1
You can only modify health from inside the actor, for example, using the getSick method:
userHealth.getSick()

How to Use Actors using async and await Feature?

Now that you have a UserHealth actor, it's time to put it to use.

Where is async keyword in Actor?

You don’t need to explicitly mark actor member using async keyword. Actor exposes all non constant member as an asynchronous one so whenever you use it, the compiler will detect that you call an async member of actor.
actor UserHealth { func getSick() } // similar to class UserHealth { func getSick() async }
This is baked inside the compiler for your convenience.

Use await Keyword to Call Actor Instance Members

Because Actor exposes its member as an async member, you utilize the await keyword to use it in every place.
await print(userHealth.health)
The compiler will halt compilation and prompt you to add the await keyword if you forget it.
The rule mandates using await for each invocation outside the actor instance. Thus, you should use await when adding two UserHealth instances, as shown below:
actor UserHealth { func add(other: UserHealth) { health += await other.health } }
Calling an actor is akin to sending a message to its "inbox". It requests a mutation or retrieval of the actor's state, but other parts of the program might still be using it, possibly from different threads. Consequently, you must wait until those threads complete their execution.

Constant Members Can Be Accessed Without await

If an id is a constant value for UserHealth, you can access it without the await keyword:
actor UserHealth { let id = UUID() }
Since the id is constant, you can access it without the await keyword:
print(userHealth.id) await print(userHealth.health)

Can I Use Swift Actor as a Class Replacement?

You can use Swift actor as class replacement. However, you will lose all the capabilities offered by the class. On the other hand, you should deal with the limitations of the actor, such as the fact that you cannot mutate its variables directly or call the method in a synchronous manner.
I advise you not to blindly use actors. Every Swift feature is designed in a special way that is unique. They are not meant to be generally used. You should use your judgment as a software engineer when using the features offered by Swift.

Conclusion

Actors offer a safe way for developers to prevent data races in their programs. If you find yourself needing a lock, consider implementing it using an actor instead.
The async and await features of Swift forces you to use Actor instance in concurrent manner but the safety already checked during compilation time.

Discussion (0)

Related Posts