In my previous article, you have learned that actors can help us in preventing data races by ensuring mutual exclusion to its mutable states. This statement is true as long as we are accessing the mutable states within the actors. If the mutable states are accessible outside of the actors, a data race can still occur!
In this article, let’s explore how this kind of data race can happen and how the Sendable
protocol can help in preventing that. On top of that, we will also take a look at the future improvements that Apple will bring to Sendable
in order to tackle this kind of situation.
So, without further ado, let’s begin!
Data Races When Passing Data Out of Actors
Let’s say we have an Article
class that has a likeCount
variable that keeps track of the number of likes the article gets from its reader.
final class Article {
let title: String
var likeCount = 0
init(title: String) {
self.title = title
}
}
and we also have an ArticleManager
actor that manages an array of article:
actor ArticleManager {
private let articles = [
Article(title: "Swift Senpai Article 01"),
Article(title: "Swift Senpai Article 02"),
Article(title: "Swift Senpai Article 03"),
]
/// Increase like count by 1
func like(_ articleTitle: String) {
guard let article = getArticle(with: articleTitle) else {
return
}
article.likeCount += 1
}
/// Get article based on article title
func getArticle(with articleTitle: String) -> Article? {
return articles.filter({ $0.title == articleTitle }).first
}
}
Notice that the ArticleManager
actor has a like(_:)
function that increases the likeCount
of a specific article, and a getArticle(with:)
function that returns an article based on the given article title.
Due to the existence of the getArticle(with:)
function, the ArticleManager
‘s articles are now accessible outside of the actor. In other words, the actor’s mutable state can now be updated outside of the actor, thus creating the potential for data races.
Now, consider the following dislike(_:)
function that lives outside of the actor:
let manager = ArticleManager()
/// Access article outside of the actor and reduces its like count by 1
func dislike(_ articleTitle: String) async {
guard let article = await manager.getArticle(with: articleTitle) else {
return
}
// Reduce like count
article.likeCount -= 1
}
Since we are now reducing (mutating) the article’s like count outside of the actor, if we try to run the actor’s like(_:)
and the above dislike(_:)
function concurrently, a data race will occur!
let articleTitle = "Swift Senpai Article 01"
// Create a parent task
Task {
// Create a task group
await withTaskGroup(of: Void.self, body: { taskGroup in
// Create 3000 child tasks to like
for _ in 0..<3000 {
taskGroup.addTask {
await self.manager.like(articleTitle)
}
}
// Create 1000 child tasks to dislike
for _ in 0..<1000 {
taskGroup.addTask {
await self.dislike(articleTitle)
}
}
})
print("👍🏻 Like count: \(await manager.getArticle(with: articleTitle)!.likeCount)")
}
In the above code, we created 3000 child tasks to like an article, and 1000 child tasks to dislike the same article concurrently. In the end, even though we are able to get an output of “👍🏻 Like count: 2000
“, the Xcode thread sanitizer will still show a threading issue, indicating that a data race does in fact occur.
Pro Tip:
You can enable the Thread Sanitizer by navigating to Product > Scheme > Edit Scheme… After that, in the edit scheme dialog choose Run > Diagnostic > check the Thread Sanitizer checkbox.
Now that you have seen how mutating the actor states outside of the actor can cause data races, what can we do to prevent this from happening?
Check Sendable by Adding a Conformance
Sendable
is a new protocol introduced in Swift 5.5 alongside async/await and actors. A Sendable
type is one whose values can be shared across different actors. If we copy a value from one place to another, and both places can safely modify their own copies of that value without interfering with each other, then that type is considered a Sendable
type. To find out more, you can take a look at this WWDC video.
Back to our sample code, in order to avoid data races when mutating an article outside of the ArticleManager
, we must conform the Article
type to the Sendable
protocol. Let’s go ahead and do that.
final class Article: Sendable {
let title: String
var likeCount = 0
init(title: String) {
self.title = title
}
}
At this stage, the compiler will start complaining about “Stored property ‘likeCount’ of ‘Sendable’-conforming class ‘Article’ is mutable“.
This error means that if we want the Article
type to be Sendable
, we cannot have a stored property (likeCount
) that is mutable. This is because it is not safe to share reference types with mutable stored properties concurrently, it will create the potential for data races.
For our case, we definitely cannot make likeCount
as constant, so what other option do we have? According to Apple, there are many different kinds of types that are sendable:
One option that we have is to change Article
to become a value type.
struct Article: Sendable {
let title: String
var likeCount = 0
init(title: String) {
self.title = title
}
}
This time, the Article
struct does not give us any compiler error, but we do get multiple compiler errors at some other places of our sample code.
Fixing all these compiler errors does require a fair amount of code change, but it is definitely doable. Therefore, I will leave this as an exercise for you. If you do get stuck, feel free to get the solution here.
As you can see from the example above, conforming to the Sendable
protocol does not automatically make one data type sendable. However, it does enforce the rules that we need to follow in order to not accidentally create a potential for data races. Therefore, next time when you are working on actors, make sure to conform any sharable mutable states to the Sendable
protocol.
Future Improvement to Expect
In WWDC, Apple mentioned that in the future, the Swift compiler will prevent us from sharing non-Sendable types. If we try to access an actor non-Sendable state, we will get a compiler error as shown below:
As of Swift 5.5, this checking is still not available, and there is no information from Apple yet as to when this will be available. For now, the best thing we can do is to be mindful and always conform to the Sendable
protocol whenever there are sharable types within an actor.
Wrapping Up
In our ArticleManager
example, the best way to prevent data races is definitely by moving the entire dislike(_:)
function into the ArticleManager
. For demonstration purposes, I purposely use the Sendable
approach so that you can better understand how the Sendable
protocol can help prevent data races outside of an actor.
At the time of writing this article, the Swift engineering team is still actively making improvements on Sendable
checking. If you have any opinions or thoughts that you would like to share, make sure to reach out to them in the Swift Forum.
Last but not least, you can get the full sample code here.
Do you find this article helpful? If you do, feel free to check out my other articles that are related to Swift concurrency:
- The Actor Reentrancy Problem in Swift
- Preventing Data Races Using Actors in Swift
- Making Network Requests with Async/await in Swift
- Getting Started with Swift Concurrency
For more articles related to iOS development and Swift, make sure to follow me on Twitter and subscribe to my monthly newsletter.
Thanks for reading. 👨🏻💻
👋🏻 Hey!
While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.
- ✨ Bartender: Superpower your menu bar and take full control over your menu bar items.
- ✨ CleanShot X: The best screen capture app I’ve ever used.
- ✨ PixelSnap: Measure on-screen elements with ease and precision.
- ✨ iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.