/ SWIFT

Atomic Properties in Swift

Although Swift has no language features for defining atomic properties, their lack is compensated with the diversity of locking APIs available in Apple’s frameworks. In this article let’s take a look at different ways of designing atomic properties in Swift.

First off, make sure we understand the core concepts related to concurrent programming and atomicity.

Concurrency and Multitasking

Concurrency refers to the ability of different parts of a program to be executed out-of-order or in partial order, without affecting the final outcome.

This allows for multitasking which is a parallel execution of the concurrent units that significantly boosts performance of a program in multi-processor systems.

Synchronization

In common sense, synchronization means making two things happen at the same time.

In programming, synchronization has broader definition: it refers to relationships among events — any number of events, and any kind of relationship (before, during, after).

As programmers, we are often concerned with synchronization constraints, which are requirements pertaining to the order of events.

Example of a constraint: Events A and B must not happen at the same time.

Atomicity

An operation is atomic if it appears to the rest of the system to occur at a single instant without being interrupted. An atomic operation can either complete or return to its original state.

Atomicity is a safety measure which enforces that operations do not complete in an unpredictable way when accessed by multiple threads or processes simultaneously.

On a software level, a common tool to enforce atomicity is lock.

Kinds of Locks

When dealing with iOS apps, we are always sandboxed to their processes. A process creates and manages threads, which are the main building blocks in multitasking iOS applications.

Lock is an abstract concept for threads synchronization. The main idea is to protect access to a given region of code at a time. Different kinds of locks exist:

  1. Semaphore — allows up to N threads to access a given region of code at a time.
  2. Mutex — ensures that only one thread is active in a given region of code at a time. You can think of it as a semaphore with a maximum count of 1.
  3. Spinlock — causes a thread trying to acquire a lock to wait in a loop while checking if the lock is available. It is efficient if waiting is rare, but wasteful if waiting is common.
  4. Read-write lock — provides concurrent access for read-only operations, but exclusive access for write operations. Efficient when reading is common and writing is rare.
  5. Recursive lock — a mutex that can be acquired by the same thread many times.

Overview of Apple Locking APIs

Lock

NSLock and its companion NSRecursiveLock are Objective-C lock classes. They correspond to Mutex and Recursive Lock and don’t have their Swift counterparts.

A lower-level C pthread_mutex_t is also available in Swift. It can be configured both as a mutex and a recursive lock.

Spinlock

OSSpinLock has been deprecated in iOS 10 and now there is no exact match to a spinlock in Swift. The closest replacement is os_unfair_lock which doesn’t spin on contention, but instead waits in the kernel to be awoken by an unlock. Thus, it has lower CPU impact than the spinlock does, but makes starvation of waiters a possibility.

Read-write lock

pthread_rwlock_t is a lower-level read-write lock that can be used in Swift.

Semaphore

DispatchSemaphore provides an implementation of a semaphore. It is listed here for the sake of completeness, as it makes little sense to use semaphore for designing atomic properties.

Implementing Atomic Property with Locks

All locks can be generalized using following protocol:

protocol Lock {
    func lock()
    func unlock()
}

Next, implement concrete locks:

extension NSLock: Lock {}

final class SpinLock: Lock {
    private var unfairLock = os_unfair_lock_s()

    func lock() {
        os_unfair_lock_lock(&unfairLock)
    }

    func unlock() {
        os_unfair_lock_unlock(&unfairLock)
    }
}

final class Mutex: Lock {
    private var mutex: pthread_mutex_t = {
        var mutex = pthread_mutex_t()
        pthread_mutex_init(&mutex, nil)
        return mutex
    }()

    func lock() {
        pthread_mutex_lock(&mutex)
    }

    func unlock() {
        pthread_mutex_unlock(&mutex)
    }
}

Now we can use any from the above locks to make the property atomic:

struct AtomicProperty {
    private var underlyingFoo = 0
    private let lock: Lock

    init(lock: Lock) {
        self.lock = lock
    }

    var foo: Int {
        get {
            lock.lock()
            let value = underlyingFoo
            lock.unlock()
            return value
        }
        set {
            lock.lock()
            underlyingFoo = newValue
            lock.unlock()
        }
    }
}

// Usage
let sample = AtomicProperty(lock: SpinLock())
_ = sample.foo

Here is how the same idea can be applied to a read-write lock:

final class ReadWriteLock {
    private var rwlock: pthread_rwlock_t = {
        var rwlock = pthread_rwlock_t()
        pthread_rwlock_init(&rwlock, nil)
        return rwlock
    }()
    
    func writeLock() {
        pthread_rwlock_wrlock(&rwlock)
    }
    
    func readLock() {
        pthread_rwlock_rdlock(&rwlock)
    }
    
    func unlock() {
        pthread_rwlock_unlock(&rwlock)
    }
}

class ReadWriteLockAtomicProperty {
    private var underlyingFoo = 0
    private let lock = ReadWriteLock()
    
    var foo: Int {
        get {
            lock.readLock()
            let value = underlyingFoo
            lock.unlock()
            return value
        }
        set {
            lock.writeLock()
            underlyingFoo = newValue
            lock.unlock()
        }
    }
}

The main bullet points from the above code are:

  1. Instead of using different locking APIs directly, we wrap them into classes conforming to the Lock interface: SpinLock and Mutex.
  2. AtomicProperty is a simple class that has atomic property foo backed by underlyingFoo under the hood.
  3. By means of lock / unlock dance we create a critical section that accesses underlyingFoo.
  4. We create separate wrapper for read-write lock, as it needs different locking APIs to be used for setter and getter.

Despite POSIX pthread locks are value types, you should not copy them both explicitly with the assignment operator or implicitly by capturing them in a closure or embedding in another value type. In POSIX, the behavior of the copy is undefined. That’s why locks are wrapped into a class instead of a struct.

Implementing Atomic Property Wrapper

You must have noticed that our atomic properties share a common pattern. Instead of repeating the boilerplate for every new property, we can design a general-purpose solution with the help of Swift property wrappers. Not to repeat myself, here I describe how to implement Swift atomic property wrapper.

Wrapping Up

Atomic operations appear to be instant from the perspective of all other threads in the app.

Despite Swift lacks default language traits for creating atomic property, it can be easily achieved with a number of available locking APIs. NSLock, dispatch and operation queues and multiple POSIX types are the most notable ones.

When dealing with POSIX locks, a rule of thumb is not to copy them and wrap in Swift APIs hiding implementation details.

If interested in performance characteristics of discussed locking APIs, I recommend checking Benchmarking Swift Locking APIs.


Thanks for reading!

If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.

Vadim Bulavin

Creator of Yet Another Swift Blog. Coding for fun since 2008, for food since 2012.

Follow