You are currently viewing How Sendable Can Help in Preventing Data Races

How Sendable Can Help in Preventing Data Races

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“.

Compiler error when conforming to Sendable protocol in Swift
Compiler error when conforming to Sendable protocol

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.

Other error in the code after changing Article to struct
Other error in the code after changing Article to struct

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:

Compiler error when try to share non-Sendable types from actors in Swift
Compiler error when try to share non-Sendable types from actors
Source: Protect mutable state with Swift actors

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:

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.