While already mentioned a few times in previous lessons, I wanted to create a dedicated lesson on custom locking mechanisms combined with the Sendable protocol. It’s very likely that you’re working in an exisiting codebase with several locking solutions in place. It’s not always needed to rewrite these to Swift Concurrency, especially if you know the code is stable and doesn’t result in data races.

There are several locking mechanisms like NSLock and DispatchQueue-based APIs. It doesn’t really matter which lock you use, the main purpose of this lesson is to show you how you can adopt these classes to Swift Concurrency.

An example of a locking mechanism

A classic example when explaining locks is a code example representing a bank account:

final class BankAccount {
    private var balance: Int = 0
    private let lock = NSLock()

    func deposit(amount: Int) {
        lock.lock()
        balance += amount
        lock.unlock()
    }

    func withdraw(amount: Int) {
        lock.lock()
        if balance >= amount {
            balance -= amount
        }
        lock.unlock()
    }

    func getBalance() -> Int {
        lock.lock()
        let currentBalance = balance
        lock.unlock()
        return currentBalance
    }
}

Whenever you access values from the bank account, you’ll have to go through one of the three methods. There’s no other way to access the balance property. Each of those methods makes use of the lock. Without discussing whether NSLock is the best lock to use, it’s at least the best lock to explain the concept.

Whenever you access mutable data, you lock access via the NSLock. Once you’re done accessing the balance, you’ll have to unlock access for others to access the balance property.

It’s pretty useful to see this example as it will help you better understand actors later on in this course.

Deciding between refactoring or @unchecked Sendable

When you’re writing new code, you should try to use actors instead of custom locking mechanisms as much as possible. This helps you reduce tech debt and lowers the amount of work you’ll have to do when migrating to Swift 6 eventually.

Existing code, however, is a different story. It might not make sense at this point to migrate to an actor instead, so you can decide to use @unchecked Sendable for the time being:

final class BankAccount: @unchecked Sendable {
    /// ...
}

This allows you to continue your work while not having to fight many concurrency warnings you would otherwise run into if you would convert this class to an actor. Though, do open a ticket in your issue tracker to make not of “Migrate BankAccount to an actor” so that you’ll be able to pick this up at a later point.

If you believe BankAccount is quite isolated and not used in many places, you could decide to migrate it to an actor right away:

actor BankAccount {
    private var balance: Int = 0

    func deposit(amount: Int) {
        balance += amount
    }

    func withdraw(amount: Int) {
        if balance >= amount {
            balance -= amount
        }
    }

    func getBalance() -> Int {
        return balance
    }
}

This might look like it’s a nobrainer and pretty doable, however, all code that accesses BankAccount now suddenly need to access it via concurrency and await. This might not be a simple migration and depending on your code structure, could lead to a lot of time spent. This can be inconvenient if you’re working on a feature request that needs to be implemented within a certain timespan.

Summary

This lesson gave a dedicated insight into deciding between locking mechanisms, @unchecked Sendable, and migrating a class to an actor. It’s a preview into what the migration module will deliver later on in this course, where we will dive much deeper into migration questions. Depending on the time available and the code in place, you could refactor right away or open an issue instead.

That’s it for this module! In the next module, it’s finally time to dive into actors.