Discover Actors in Swift
You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿
You’ve heard that actors are a very powerful feature of Swift, but you're not really sure how they work? 🤨
Don’t worry! I’ve got you covered! 😌
In just a few paragraphs we’ll go over everything you need to understand how to use an actor
in Swift!
Let’s start with this class that implements a simple cache of images:
As long as all our call sites are happening on a single thread of execution, everything is working fine 👌
But let’s try and introduce a bit of concurrency!
Our ImageCache
is now vulnerable to data races, because it’s very likely that both Task
will attempt to mutate its internal dictionary at the same time 😞
So how can we protect against such data races?
The typical solution used to be the following.
First create a serial execution queue:
Then synchronize all read and write accesses to the internal state through this serial queue:
This approach works, but at the price of added complexity at the call sites because reading data now has to be done through a completion handler:
And this is when actors come into play!
Let’s go back to our ImageCache
and remove all the code we’ve added to manually synchronize the access to the internal state…
…and now let’s replace the keyword class
by the keyword actor
:
Thanks to this simple change, all method calls will now be automatically synchronized so that they are executed one at a time, just like when we were using an explicit execution queue!
As an added bonus, the call sites have also become simpler: the completion handler is gone and we only need to use await
when calling a method from our actor
:
And that’s it, we’ve covered the basics of how actors work in Swift!
Thanks to this keyword, we are able to easily make a type thread-safe by ensuring all method and property calls happen one at a time 👌
Here’s the code if you want to experiment with it:
actor ImageCache {
private var cache = [UUID: UIImage]()
func save(image: UIImage, withID id: UUID) {
cache[id] = image
}
func getImage(for id: UUID) -> UIImage? {
cache[id]
}
}
let imageCache = ImageCache()
Task.detached {
await imageCache.save(image: firstImage, withID: firstImageID)
}
Task.detached {
await imageCache.save(image: secondImage, withID: secondImageID)
}
let cachedImage = await imageCache.getImage(for: firstImageID)