When you begin utilizing Swift Concurrency, actors will basically change into your customary alternative for safeguarding mutable state. Nonetheless, introducing actors additionally tends to introduce extra concurrency than you meant which may result in extra complicated code, and a a lot tougher time transitioning to Swift 6 in the long term.
Whenever you work together with state that’s protected by an actor, you need to to take action asynchronously. The result’s that you simply’re writing asynchronous code in locations the place you may by no means have meant to introduce concurrency in any respect.
One approach to resolve that’s to annotate your as an instance view mannequin with the @MainActor
annotation. This makes certain that each one your code runs on the primary actor, which signifies that it is thread-safe by default, and it additionally makes certain which you can safely work together together with your mutable state.
That stated, this won’t be what you are searching for. You may wish to have code that does not run on the primary actor, that is not remoted by international actors or any actor in any respect, however you simply wish to have an old school thread-safe property.
Traditionally, there are a number of methods during which we will synchronize entry to properties. We used to make use of Dispatch Queues, for instance, when GCD was the usual for concurrency on Apple Platforms.
Just lately, the Swift crew added one thing referred to as a Mutex to Swift. With mutexes, now we have a substitute for actors for safeguarding our mutable state. I say various, but it surely’s probably not true. Actors have a really particular function in that they defend our mutable state for a concurrent atmosphere the place we would like code to be asynchronous. Mutexes, alternatively, are actually helpful once we don’t need our code to be asynchronous and when the operation we’re synchronizing is fast (like assigning to a property).
On this publish, we’ll discover the way to use Mutex
, when it is helpful, and the way you select between a Mutex
or an actor.
Mutex utilization defined
A Mutex
is used to guard state from concurrent entry. In most apps, there might be a handful of objects that is perhaps accessed concurrently. For instance, a token supplier, an picture cache, and different networking-adjacent objects are sometimes accessed concurrently.
On this publish, I’ll use a quite simple Counter
object to verify we don’t get misplaced in complicated particulars and specifics that don’t impression or change how we use a Mutex
.
Whenever you increment or decrement a counter, that’s a fast operation. And in a codebase the place. the counter is on the market in a number of duties on the similar time, we would like these increment and decrement operations to be protected and free from knowledge races.
Wrapping your counter in an actor is smart from a concept perspective as a result of we would like the counter to be protected against concurrent accesses. Nonetheless, once we do that, we make each interplay with our actor asynchronous.
To considerably forestall this, we may constrain the counter to the primary actor, however that signifies that we’re at all times going to need to be on the primary actor to work together with our counter. We would not at all times be on the identical actor once we work together with our counter, so we’d nonetheless need to await interactions in these conditions, and that is not superb.
As a way to create a synchronous API that can be thread-safe, we may fall again to GCD and have a serial DispatchQueue
.
Alternatively, we will use a Mutex
.
A Mutex
is used to wrap a bit of state and it ensures that there is unique entry to that state. A Mutex
makes use of a lock below the hood and it comes with handy strategies to ensure that we purchase and launch our lock rapidly and accurately.
After we attempt to work together with the Mutex
‘ state, now we have to attend for the lock to change into accessible. That is much like how an actor would work with the important thing distinction being that ready for a Mutex
is a blocking operation (which is why we should always solely use it for fast and environment friendly operations).
This is what interacting with a Mutex
seems like:
class Counter {
personal let mutex = Mutex(0)
func increment() {
mutex.withLock { rely in
rely += 1
}
}
func decrement() {
mutex.withLock { rely in
rely -= 1
}
}
}
Our increment
and decrement
capabilities each purchase the Mutex
, and mutate the rely
that’s handed to withLock
.
Our Mutex
is outlined by calling the Mutex
initializer and passing it our preliminary state. On this case, we cross it 0
as a result of that’s the beginning worth for our counter.
On this instance, I’ve outlined two capabilities that safely mutate the Mutex
‘ state. Now let’s see how we will get the Mutex
‘ worth:
var rely: Int {
return mutex.withLock { rely in
return rely
}
}
Discover that studying the Mutex
worth can be executed withLock
. The important thing distinction with increment
and decrement
right here is that as a substitute of mutating rely
, I simply return it.
It’s completely important that we hold our operations inside withLock
quick. We don’t wish to maintain the lock for any longer than we completely need to as a result of any threads which might be ready for our lock or blocked whereas we maintain the lock.
We are able to increase our instance a bit bit by including a get
and set
to our rely
. This can permit customers of our Counter
to work together with rely
prefer it’s a standard property whereas we nonetheless have data-race safety below the hood:
var rely: Int {
get {
return mutex.withLock { rely in
return rely
}
}
set {
mutex.withLock { rely in
rely = newValue
}
}
}
We are able to now use our Counter
as follows:
let counter = Counter()
counter.rely = 10
print(counter.rely)
That’s fairly handy, proper?
Whereas we now have a kind that is freed from data-races, utilizing it in a context the place there are a number of isolation contexts is a little bit of a problem once we opt-in to Swift 6 since our Counter
doesn’t conform to the Sendable
protocol.
The great factor about Mutex
and sendability is that mutexes are outlined as being Sendable
in Swift itself. Because of this we will replace our Counter
to be Sendable
fairly simply, and while not having to make use of @unchecked Sendable
!
remaining class Counter: Sendable {
personal let mutex = Mutex(0)
// ....
}
At this level, now we have a reasonably good setup; our Counter
is Sendable
, it’s freed from data-races, and it has a totally synchronous API!
After we try to use our Counter
to drive a SwiftUI view by making it @Observable
, this get a bit difficult:
struct ContentView: View {
@State personal var counter = Counter()
var physique: some View {
VStack {
Textual content("(counter.rely)")
Button("Increment") {
counter.increment()
}
Button("Decrement") {
counter.decrement()
}
}
.padding()
}
}
@Observable
remaining class Counter: Sendable {
personal let mutex = Mutex(0)
var rely: Int {
get {
return mutex.withLock { rely in
return rely
}
}
set {
mutex.withLock { rely in
rely = newValue
}
}
}
}
The code above will compile however the view received’t ever replace. That’s as a result of our computed property rely
relies on state that’s not explicitly altering. The Mutex
will change the worth it protects however that doesn’t change the Mutex
itself.
In different phrases, we’re not mutating any knowledge in a manner that @Observable
can “see”.
To make our computed property work @Observable
, we have to manually inform Observable
once we’re accessing or mutating (on this case, the rely keypath). This is what that appears like:
var rely: Int {
get {
self.entry(keyPath: .rely)
return mutex.withLock { rely in
return rely
}
}
set {
self.withMutation(keyPath: .rely) {
mutex.withLock { rely in
rely = newValue
}
}
}
}
By calling the entry
and withMutation
strategies that the @Observable
macro provides to our Counter
, we will inform the framework once we’re accessing and mutating state. This can tie into our Observable
’s common state monitoring and it’ll permit our views to replace once we change our rely
property.
Mutex or actor? The best way to determine?
Selecting between a mutex and an actor is just not at all times trivial or apparent. Actors are actually good in concurrent environments when you have already got an entire bunch of asynchronous code. When you do not wish to introduce async code, or while you’re solely defending one or two properties, you are in all probability within the territory the place a mutex makes extra sense as a result of the mutex won’t pressure you to jot down asynchronous code wherever.
I may fake that this can be a trivial determination and it’s best to at all times use mutexes for easy operations like our counter and actors solely make sense while you wish to have an entire bunch of stuff working asynchronously, however the determination normally is not that easy.
By way of efficiency, actors and mutexes do not fluctuate that a lot, so there’s not an enormous apparent efficiency profit that ought to make you lean in a single path or the opposite.
In the long run, your alternative ought to be based mostly round comfort, consistency, and intent. If you happen to’re discovering your self having to introduce a ton of async code simply to make use of an actor, you are in all probability higher off utilizing a Mutex
.
Actors ought to be thought-about an asynchronous instrument that ought to solely be utilized in locations the place you’re deliberately introducing and utilizing concurrency. They’re additionally extremely helpful while you’re making an attempt to wrap longer-running operations in a manner that makes them thread-safe. Actors don’t block execution which signifies that you’re fully positive with having “slower” code on an actor.
When unsure, I prefer to strive each for a bit after which I keep on with the choice that’s most handy to work with (and sometimes that’s the Mutex
…).
In Abstract
On this publish, you have realized about mutexes and the way you need to use them to guard mutable state. I confirmed you the way they’re used, after they’re helpful, and the way a Mutex
compares to an actor.
You additionally realized a bit bit about how one can select between an actor or a property that is protected by a mutex.
Making a alternative between an actor or a Mutex
is, in my view, not at all times simple however experimenting with each and seeing which model of your code comes out simpler to work with is an effective begin while you’re making an attempt to determine between a Mutex
and an actor.