Swift Synchronization is a collection of Swift macros used to protect shared mutable state.
class Locks {
@Locked(.checked)
var count: Int
init(count: Int) {
self.count = count
}
}
To add Swift Synchronization to a Swift package manifest file:
- Add the
swift-synchronization
package to your package'sdependencies
:.package( url: "https://github.com/fetch-rewards/swift-synchronization.git", from: "<#latest swift-synchronization tag#>" )
- Add the
Synchronization
product to your target'sdependencies
:.product(name: "Synchronization", package: "swift-synchronization")
Import Synchronization
:
import Synchronization
Attach the @Locked
macro to your property:
@Locked(.checked)
var count: Int
And that's it! Access to your property's underlying data is now synchronized using an
OSAllocatedUnfairLock
.
You can continue to use your property just as you normally would, without ever needing to directly
access the private, underscored property that is managing mutual exclusion for you.
Important
The property to which @Locked
is attached must be a var
and must have an explicit type:
// Valid:
var count: Int = .zero
var count = Int.zero
var count = Int(1)
// Invalid:
var count = 1
let count: Int = .zero
@Locked
must be initialized with a lockType
. If your property's type conforms to
Sendable
, use @Locked(.checked)
,
otherwise use @Locked(.unchecked)
.
Warning
@Locked
uses OSAllocatedUnfairLock
to protect shared
mutable state. This is useful when you need fast, low-level mutual exclusion and can manage the following limitations:
- The lock is unfair by design. It may repeatedly favor certain threads over others. This can result in:
- Thread Starvation: Some threads might experience indefinite delays in acquiring the lock under high contention.
- Unpredictable Ordering: Operations that are meant to be “relative” (e.g. thread A before thread B) may not happen in that order. This can lead to unexpected behavior when the sequence of operations matters.
OSAllocatedUnfairLock
only guarantees mutual exclusion, not memory ordering beyond what is needed for the lock.- If you perform complex relative logic across multiple locks or shared state, you may still get races or undefined behavior, especially without proper memory barriers.
- Locking does not prevent logic bugs.
- You might lock correctly but still compare stale values.
- You could perform multiple operations atomically in your mind, but they’re not in reality.
For more complex synchronization requirements or when fairness is crucial, consider using higher-level constructs provided by Swift concurrency or Grand Central Dispatch.
Swift Synchronization contains the Swift macro @Locked
.
@Locked
is an attached peer and accessor macro that generates a private, protected, underscored backing property along
with accessors for reading from and writing to that backing property:
@Locked(.checked)
var count: Int
// Generates:
var count: Int {
@storageRestrictions(initializes: _count)
init(initialValue) {
self._count = OSAllocatedUnfairLock<Int>(
initialState: initialValue
)
}
get {
self._count.withLock { count in
count
}
}
set {
self._count.withLock { count in
count = newValue
}
}
}
private let _count: OSAllocatedUnfairLock<Int>
The backing property uses OSAllocatedUnfairLock
to synchronize
access to its underlying data while the exposed property's accessors allow consumers to interact with this data without interfacing
directly with OSAllocatedUnfairLock
's API.
@Locked
takes a single argument: lockType
. The possible values are .checked
and .unchecked
.
If your property's type conforms to Sendable
, use .checked
:
@Locked(.checked)
var count: Int
// Generates:
var count: Int {
@storageRestrictions(initializes: _count)
init(initialValue) {
self._count = OSAllocatedUnfairLock<Int>(
initialState: initialValue
)
}
get {
self._count.withLock { count in
count
}
}
set {
self._count.withLock { count in
count = newValue
}
}
}
private let _count: OSAllocatedUnfairLock<Int>
If your property's type does not conform to Sendable
, use .unchecked
:
@Locked(.unchecked)
var nonSendableInstance: NonSendableType
// Generates:
var nonSendableInstance: NonSendableType {
@storageRestrictions(initializes: _nonSendableInstance)
init(initialValue) {
self._nonSendableInstance = OSAllocatedUnfairLock<NonSendableType>(
uncheckedState: initialValue
)
}
get {
self._nonSendableInstance.withLockUnchecked { nonSendableInstance in
nonSendableInstance
}
}
set {
self._nonSendableInstance.withLockUnchecked { nonSendableInstance in
nonSendableInstance = newValue
}
}
}
private let _nonSendableInstance: OSAllocatedUnfairLock<NonSendableType>
The property to which @Locked
is attached can be defined with (var count: Int = .zero
) or without (var count: Int
)
a default value.
Providing a default value (var count: Int = .zero
) results in two generated accessors - get
and set
:
get {
self._count.withLock { count in
count
}
}
set {
self._count.withLock { count in
count = newValue
}
}
Not providing a default value (var count: Int
) results in an additional generated accessor - init
:
@storageRestrictions(initializes: _count)
init(initialValue) {
self._count = OSAllocatedUnfairLock<Int>(
initialState: initialValue
)
}
This init
accessor allows you to assign a value to your property inside your object's initializer:
class Locks {
@Locked(.checked)
var count: Int
init(count: Int) {
self.count = count
}
}
The simplest way to contribute to this project is by opening an issue.
If you would like to contribute code to this project, please read our Contributing Guidelines.
By opening an issue or contributing code to this project, you agree to follow our Code of Conduct.
This library is released under the MIT license. See LICENSE for details.