When multiple threads access shared resources—such as the same object, variable, or file—data corruption and race conditions can easily occur. This fundamental concurrency problem is illustrated by classic examples like bank transactions and ticket selling.
Analyzing the Race Condition
Consider an accountBalance property modified by concurrent save and withdraw operations. The following Swift example uses GCD to simulate the issue:
class BankDemo {
var balance: Int = 100
func save() {
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp + 50
print("Deposited 50, balance: \(balance) - \(Thread.current)")
}
func withdraw() {
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp - 20
print("Withdrew 20, balance: \(balance) - \(Thread.current)")
}
func run() {
let queue = DispatchQueue.global()
for _ in 0..<10 {
queue.async { self.save() }
queue.async { self.withdraw() }
}
}
}
Without synchronization, the final balance is unpredictable because read‑modify‑write sequences interleave.
Thread Synchronization in iOS
To enforce ordered access, iOS offers several synchronization primitives. The core idea is to protect critical sections with locks or serial execution.
| Mechanism | Notes |
|---|---|
os_unfair_lock |
Replacement for OSSpinLock; waiting threads sleep instead of busy‑waiting. |
OSSpinLock |
Deprecated – prone to priority inversion; not recommended. |
pthread_mutex |
POSIX mutex; supports normal, recursive, and error‑checking types. |
NSLock |
Objective‑C wrapper around a normal mutex. |
NSRecursiveLock |
Wrapper around a recursive mutex; allows the same thread to lock multiple times. |
NSCondition |
Wraps a mutex and a condition variable; anables signaling between threads. |
NSConditionLock |
Extends NSCondition with a condition value. |
@synchronized |
Mutual exclusion using an object as a lock; based on a recursive pthread_mutex. |
dispatch_semaphore |
Controls access count; a semaphore with value 1 acts as a mutex. |
dispatch_queue (serial) |
Serial queues synchronize tasks by executing them one at a time. |
Using os_unfair_lock
import os.lock
class LockDemo {
private var lock = os_unfair_lock_s()
private var balance = 100
func save() {
os_unfair_lock_lock(&lock)
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp + 50
print("Deposited, balance: \(balance)")
os_unfair_lock_unlock(&lock)
}
func withdraw() {
os_unfair_lock_lock(&lock)
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp - 20
print("Withdrew, balance: \(balance)")
os_unfair_lock_unlock(&lock)
}
}
Using pthread_mutex
import Foundation
class MutexDemo {
private var mutex = pthread_mutex_t()
private var balance = 100
init() {
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)
pthread_mutex_init(&mutex, &attr)
pthread_mutexattr_destroy(&attr)
}
deinit {
pthread_mutex_destroy(&mutex)
}
func save() {
pthread_mutex_lock(&mutex)
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp + 50
pthread_mutex_unlock(&mutex)
}
// withdraw similar
}
Using NSLock
class NSLockDemo {
private let moneyLock = NSLock()
private var balance = 100
func save() {
moneyLock.lock()
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp + 50
moneyLock.unlock()
}
}
Using a Serial Dispatch Queue
class SerialQueueDemo {
private let queue = DispatchQueue(label: "com.example.money")
private var balance = 100
func save() {
queue.sync {
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp + 50
}
}
func withdraw() {
queue.sync {
let temp = balance
Thread.sleep(forTimeInterval: 0.5)
balance = temp - 20
}
}
}
Using dispatch_semaphore
class SemaphoreDemo {
private let semaphore = DispatchSemaphore(value: 1)
private var balance = 100
func save() {
semaphore.wait()
let temp = balance
balance = temp + 50
semaphore.signal()
}
}
Using @synchronized
class SynchronizedDemo {
private var balance = 100
func save() {
objc_sync_enter(self)
let temp = balance
balance = temp + 50
objc_sync_exit(self)
}
}
Performance Ranking
From highest to lowest performance (approximate):
os_unfair_lockOSSpinLock(deprecated)dispatch_semaphorepthread_mutexdispatch_queue(serial)NSLockNSConditionpthread_mutex(recursive)NSRecursiveLockNSConditionLock@synchronized
Spinlock vs. Mutex
Spinlocks (busy‑wait) are suitable when the wait time is expected to be very short, the critical section is small, and CPU resources are abundant. Mutexes (sleep‑wait) are better when wait times may be long, on single‑core processors, or when the critical section involves I/O or complex logic.
Atomic Properties
Declaring a property as atomic guarantees that its getter and setter are atomic, but it does not make the usage of that property thread‑safe. Because atomic setter/getter use a spinlock internally, they incur a performance cost and are rarely used in modern iOS development.
Read‑Write Safety (Multiple Readers, Single Writer)
For scenarios like file I/O, we often need concurrent reads but exclusive writes. iOS provides two main solutions:
pthread_rwlock
import Darwin.POSIX
class RWLockDemo {
private var rwlock = pthread_rwlock_t()
init() {
pthread_rwlock_init(&rwlock, nil)
}
deinit {
pthread_rwlock_destroy(&rwlock)
}
func read() {
pthread_rwlock_rdlock(&rwlock)
// perform read
pthread_rwlock_unlock(&rwlock)
}
func write() {
pthread_rwlock_wrlock(&rwlock)
// perform write
pthread_rwlock_unlock(&rwlock)
}
}
dispatch_barrier_async
class BarrierDemo {
private let queue = DispatchQueue(label: "rw.queue", attributes: .concurrent)
func read() {
queue.async {
// concurrent read
}
}
func write() {
queue.async(flags: .barrier) {
// exclusive write, all pending reads finish first
}
}
}
Note: dispatch_barrier_async must be used with a custom concurrent queue.
NSMutableArray and Thread Safety
NSMutableArray is not thread‑safe. Mutations (add, remove, replace) must be protected by a lock. Even a read operation (count, iteration) can observe inconsistent state if a concurrent mutation is in progress, so reads also require synchronization, often using a read‑write lock or barrier for optimal performance.
Deadlock
A deadlock occurs when processes are stuck waiting for each other’s resources. Four conditions must hold simultaneously:
- Mutual exclusion – resources cannot be shared.
- Hold and wait – a process holds resources while waiting for others.
- No preemption – resources cannot be forcibly taken away.
- Circular wait – a circular chain of processes exists, each waiting for a resource held by the next.
Breaking any of these conditions can resolve or prevent deadlocks. Common recovery methods include resource preemption or terminating processes.