Protecting mutable state with Mutex in Swift
Published on: April 30, 2025Once you start using Swift Concurrency, actors will essentially become your standard choice for protecting mutable state. However, introducing actors also tends to introduce more concurrency than you intended which can lead to more complex code, and a much harder time transitioning to Swift 6 in the long run.
When you interact with state that’s protected by an actor, you have to to do so asynchronously. The result is that you’re writing asynchronous code in places where you might never have intended to introduce concurrency at all.
One way to resolve that is to annotate your let's say view model with the @MainActor
annotation. This makes sure that all your code runs on the main actor, which means that it's thread-safe by default, and it also makes sure that you can safely interact with your mutable state.
That said, this might not be what you're looking for. You might want to have code that doesn't run on the main actor, that's not isolated by global actors or any actor at all, but you just want to have an old-fashioned thread-safe property.
Historically, there are several ways in which we can synchronize access to properties. We used to use Dispatch Queues, for example, when GCD was the standard for concurrency on Apple Platforms.
Recently, the Swift team added something called a Mutex to Swift. With mutexes, we have an alternative to actors for protecting our mutable state. I say alternative, but it's not really true. Actors have a very specific role in that they protect our mutable state for a concurrent environment where we want code to be asynchronous. Mutexes, on the other hand, are really useful when we don't want our code to be asynchronous and when the operation we’re synchronizing is quick (like assigning to a property).
In this post, we’ll explore how to use Mutex
, when it's useful, and how you choose between a Mutex
or an actor.
Mutex usage explained
A Mutex
is used to protect state from concurrent access. In most apps, there will be a handful of objects that might be accessed concurrently. For example, a token provider, an image cache, and other networking-adjacent objects are often accessed concurrently.
In this post, I’ll use a very simple Counter
object to make sure we don’t get lost in complex details and specifics that don’t impact or change how we use a Mutex
.
When you increment or decrement a counter, that’s a quick operation. And in a codebase where. the counter is available in several tasks at the same time, we want these increment and decrement operations to be safe and free from data races.
Wrapping your counter in an actor makes sense from a theory point of view because we want the counter to be protected from concurrent accesses. However, when we do this, we make every interaction with our actor asynchronous.
To somewhat prevent this, we could constrain the counter to the main actor, but that means that we're always going to have to be on the main actor to interact with our counter. We might not always be on the same actor when we interact with our counter, so we would still have to await interactions in those situations, and that isn't ideal.
In order to create a synchronous API that is also thread-safe, we could fall back to GCD and have a serial DispatchQueue
.
Alternatively, we can use a Mutex
.
A Mutex
is used to wrap a piece of state and it ensures that there's exclusive access to that state. A Mutex
uses a lock under the hood and it comes with convenient methods to make sure that we acquire and release our lock quickly and correctly.
When we try to interact with the Mutex
' state, we have to wait for the lock to become available. This is similar to how an actor would work with the key difference being that waiting for a Mutex
is a blocking operation (which is why we should only use it for quick and efficient operations).
Here's what interacting with a Mutex
looks like:
class Counter {
private let mutex = Mutex(0)
func increment() {
mutex.withLock { count in
count += 1
}
}
func decrement() {
mutex.withLock { count in
count -= 1
}
}
}
Our increment
and decrement
functions both acquire the Mutex
, and mutate the count
that’s passed to withLock
.
Our Mutex
is defined by calling the Mutex
initializer and passing it our initial state. In this case, we pass it 0
because that’s the starting value for our counter.
In this example, I’ve defined two functions that safely mutate the Mutex
' state. Now let’s see how we can get the Mutex
' value:
var count: Int {
return mutex.withLock { count in
return count
}
}
Notice that reading the Mutex
value is also done withLock
. The key difference with increment
and decrement
here is that instead of mutating count
, I just return it.
It is absolutely essential that we keep our operations inside of withLock
short. We do not want to hold the lock for any longer than we absolutely have to because any threads that are waiting for our lock or blocked while we hold the lock.
We can expand our example a little bit by adding a get
and set
to our count
. This will allow users of our Counter
to interact with count
like it’s a normal property while we still have data-race protection under the hood:
var count: Int {
get {
return mutex.withLock { count in
return count
}
}
set {
mutex.withLock { count in
count = newValue
}
}
}
We can now use our Counter
as follows:
let counter = Counter()
counter.count = 10
print(counter.count)
That’s quite convenient, right?
While we now have a type that is free of data-races, using it in a context where there are multiple isolation contexts is a bit of an issue when we opt-in to Swift 6 since our Counter
doesn’t conform to the Sendable
protocol.
The nice thing about Mutex
and sendability is that mutexes are defined as being Sendable
in Swift itself. This means that we can update our Counter
to be Sendable
quite easily, and without needing to use @unchecked Sendable
!
final class Counter: Sendable {
private let mutex = Mutex(0)
// ....
}
At this point, we have a pretty good setup; our Counter
is Sendable
, it’s free of data-races, and it has a fully synchronous API!
When we try and use our Counter
to drive a SwiftUI view by making it @Observable
, this get a little tricky:
struct ContentView: View {
@State private var counter = Counter()
var body: some View {
VStack {
Text("\(counter.count)")
Button("Increment") {
counter.increment()
}
Button("Decrement") {
counter.decrement()
}
}
.padding()
}
}
@Observable
final class Counter: Sendable {
private let mutex = Mutex(0)
var count: Int {
get {
return mutex.withLock { count in
return count
}
}
set {
mutex.withLock { count in
count = newValue
}
}
}
}
The code above will compile but the view won’t ever update. That’s because our computed property count
is based on state that’s not explicitly changing. The Mutex
will change the value it protects but that doesn’t change the Mutex
itself.
In other words, we’re not mutating any data in a way that @Observable
can “see”.
To make our computed property work @Observable
, we need to manually tell Observable
when we're accessing or mutating (in this case, the count keypath). Here's what that looks like:
var count: Int {
get {
self.access(keyPath: \.count)
return mutex.withLock { count in
return count
}
}
set {
self.withMutation(keyPath: \.count) {
mutex.withLock { count in
count = newValue
}
}
}
}
By calling the access
and withMutation
methods that the @Observable
macro adds to our Counter
, we can tell the framework when we’re accessing and mutating state. This will tie into our Observable
’s regular state tracking and it will allow our views to update when we change our count
property.
Mutex or actor? How to decide?
Choosing between a mutex and an actor is not always trivial or obvious. Actors are really good in concurrent environments when you already have a whole bunch of asynchronous code. When you don't want to introduce async code, or when you're only protecting one or two properties, you're probably in the territory where a mutex makes more sense because the mutex will not force you to write asynchronous code anywhere.
I could pretend that this is a trivial decision and you should always use mutexes for simple operations like our counter and actors only make sense when you want to have a whole bunch of stuff working asynchronously, but the decision usually isn't that straightforward.
In terms of performance, actors and mutexes don't vary that much, so there's not a huge obvious performance benefit that should make you lean in one direction or the other.
In the end, your choice should be based around convenience, consistency, and intent. If you're finding yourself having to introduce a ton of async code just to use an actor, you're probably better off using a Mutex
.
Actors should be considered an asynchronous tool that should only be used in places where you’re intentionally introducing and using concurrency. They’re also incredibly useful when you’re trying to wrap longer-running operations in a way that makes them thread-safe. Actors don’t block execution which means that you’re completely fine with having “slower” code on an actor.
When in doubt, I like to try both for a bit and then I stick with the option that’s most convenient to work with (and often that’s the Mutex
...).
In Summary
In this post, you've learned about mutexes and how you can use them to protect mutable state. I showed you how they’re used, when they’re useful, and how a Mutex
compares to an actor.
You also learned a little bit about how you can choose between an actor or a property that's protected by a mutex.
Making a choice between an actor or a Mutex
is, in my opinion, not always easy but experimenting with both and seeing which version of your code comes out easier to work with is a good start when you’re trying to decide between a Mutex
and an actor.