SoatDev IT Consulting
SoatDev IT Consulting
  • About us
  • Expertise
  • Services
  • How it works
  • Contact Us
  • News
  • August 14, 2023
  • Rss Fetcher

The optimal method to ensure memory safety in a multi-threaded environment

hardware becomes more powerful and compact over time… while software becomes slower and more complex
Popular wisdom

The rise of parallelism in hardware has led to significant changes in software development. This has brought about the introduction of APIs for thread management and synchronization primitives in SDKs. Crafting a multi-threaded program that runs without glitches is a challenging endeavor. Among the most daunting challenges is ensuring different threads access memory without conflict.

In true parallel systems (like multi-core or multiprocessor setups), two threads might simultaneously read from and write to the same memory location, leading to a “data race.” Delving into the finer details (such as data size, alignment, processor caches, and atomics) might get complex, but using mutexes is the conventional method to prevent data races. When a mutex instance is in place, and its lock/unlock methods are appropriately called, it ensures the atomicity of data operations and exclusive access.

On Apple’s platforms, the os_unfair_lock is the most performance-efficient lock available.

In Swift, using it can be a bit more challenging compared to other locks. Prior to iOS 16, developers had to manage memory manually to prevent creating local instance copies.

import os

public final class UnfairLock {
public init() {
self.pointer = .allocate(capacity: 1)
self.pointer.initialize(to: os_unfair_lock())
}

deinit {
self.pointer.deinitialize(count: 1)
self.pointer.deallocate()
}

public func lock() {
os_unfair_lock_lock(self.pointer)
}

public func unlock() {
os_unfair_lock_unlock(self.pointer)
}

private let pointer: os_unfair_lock_t
}

Starting with iOS 16, you can utilize the newly introduced OSAllocatedUnfairLock type. Its possible implementation might be similar to the code example provided above:

let lock = OSAllocatedUnfairLock()

lock.lock()
// ...
lock.unlock()

To avoid repeatedly placing paired lock/unlock calls throughout the code and to prevent data races, many developers utilize a feature known as “property wrappers.” This acts as syntactic sugar by encapsulating the lock/unlock logic within itself:

@propertyWrapper
public final class ThreadSafe<T> {
public init(wrappedValue: T) {
self.value = wrappedValue
}

public var projectedValue: ThreadSafe<T> { self }

public var wrappedValue: T {
get {
self.lock.lock(); defer { self.lock.unlock() }
return self.value
}
set {
self.lock.lock(); defer { self.lock.unlock() }
self.value = newValue
}
}

private let lock = UnfairLock()

private var value: T
}

But there is one implicit problem in the wrappedValue implementation:

GET and SET are atomic, but GET+SET is not

What does it mean? For example, you have the following code:

@ThreadSafe
var x: UInt64 = 0

x += 1

This line of code, x += 1, essentially consists of a “get” operation followed by a “set” operation (x = x + 1). While each of these operations is atomic individually, there’s a potential issue: another thread might update the value between the “get” and “set” operations, leading to unexpected outcomes in the program logic. Experienced developers recognize this pitfall and often employ a helper method to mitigate this issue, as shown below:

public func write<V>(_ f: (inout T) -> V) -> V {
self.lock.lock(); defer { self.lock.unlock() }
return f(&self.value)
}

Which in the example with the counter leads to the following use of the property wrapper:

@ThreadSafe
var x: Int = 5

$x.write { $0 += 1 }

The solution works, but it doesn’t eliminate the human element. This is because both expressions x += 1 and $x.write { $0 += 1 } are considered valid by the compiler.

However, there’s no need to worry. Swift’s undocumented feature allows you to merge paired “get+set” calls into a single operation. This feature is actively used within the standard Swift library and is named _modify :

var wrappedValue: T {
get { return value }
_modify { yield &self.value }
}

_modify is an accessor (like get and set) with two very important advantages:

  1. Atomicity — combining “get+set” into one call
  2. Performance — working with data in place, without copying

You can read more about _modify and yield in the original pitch and this post.

_modify is a publicly accessible API that remains undocumented. However, based on my year and a half of experience using it in a project that serves millions of users, I’ve encountered no issues.

Happy Ending

Combining unfair lock with _modify, we achieve the most convenient and optimal method for ensuring memory safety in multi-threaded environment:

public var wrappedValue: T {
get {
self.lock.lock(); defer { self.lock.unlock() }
return self.value
}
_modify {
self.lock.lock(); defer { self.lock.unlock() }
yield &self.value
}
}

...

private let lock = UnfairLock()

The code from the article is provided as a production-ready Swift package in this repository. Enjoy!


Mastering Thread Safety in Swift With One Runtime Trick was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.

Previous Post
Next Post

Recent Posts

  • From LLMs to hallucinations, here’s a simple guide to common AI terms
  • Octonions sometimes associate
  • Looking for keys under the lamppost
  • Why Intempus thinks robots should have a human physiological state
  • 48 hours left: What you won’t want to miss at the 20th TechCrunch Disrupt in October

Categories

  • Industry News
  • Programming
  • RSS Fetched Articles
  • Uncategorized

Archives

  • May 2025
  • April 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • July 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023

Tap into the power of Microservices, MVC Architecture, Cloud, Containers, UML, and Scrum methodologies to bolster your project planning, execution, and application development processes.

Solutions

  • IT Consultation
  • Agile Transformation
  • Software Development
  • DevOps & CI/CD

Regions Covered

  • Montreal
  • New York
  • Paris
  • Mauritius
  • Abidjan
  • Dakar

Subscribe to Newsletter

Join our monthly newsletter subscribers to get the latest news and insights.

© Copyright 2023. All Rights Reserved by Soatdev IT Consulting Inc.