Skip to content

Instantly share code, notes, and snippets.

@gdavis
Last active October 21, 2024 13:49
Show Gist options
  • Save gdavis/5745bb481af68791bbb8072b9b4a9711 to your computer and use it in GitHub Desktop.
Save gdavis/5745bb481af68791bbb8072b9b4a9711 to your computer and use it in GitHub Desktop.
ThreadSafe is a property wrapper that can be used to control atomic access to the underlying property while allowing concurrent access to reading the value.
//
// ThreadSafe.swift
// GDICore
//
// Created by Grant Davis on 1/2/21.
// Updated to support `_modify` accessor on 12/5/21.
//
// Copyright © 2021 Grant Davis Interactive, LLC. All rights reserved.
//
import Foundation
/// A property wrapper that uses a serial `DispatchQueue` to access the underlying
/// value of the property, and `NSLock` to prevent reading while modifying the value.
///
/// This property wrapper supports mutating properties using coroutines
/// by implementing the `_modify` accessor, which allows the same memory
/// address to be modified serially when accessed from multiple threads.
/// See: https://forums.swift.org/t/modify-accessors/31872
@propertyWrapper public struct ThreadSafe<T> {
private var _value: T
private let lock = NSLock()
private let queue: DispatchQueue
public var wrappedValue: T {
get {
queue.sync { _value }
}
_modify {
lock.lock()
var tmp: T = _value
defer {
_value = tmp
lock.unlock()
}
yield &tmp
}
}
public init(wrappedValue: T, queue: DispatchQueue? = nil) {
self._value = wrappedValue
self.queue = queue ?? DispatchQueue(label: "ThreadSafe \(String(typeName: T.self))")
}
}
// Helper extension to name the queue after the property wrapper's type.
public extension String {
init(typeName thing: Any.Type) {
let describingString = String(describing: thing)
let name = describingString.components(separatedBy: ".").last ?? ""
self.init(stringLiteral: name)
}
}
//
// ThreadSafetyTests.swift
//
//
// Created by Grant Davis on 1/23/21.
//
// Copyright © 2021 Grant Davis Interactive, LLC. All rights reserved.
//
import Foundation
import XCTest
@testable import GDICore
class ThreadSafetyTests: XCTestCase {
@ThreadSafe var values: Set<Int> = []
private lazy var queue1 = DispatchQueue(label: "ThreadSafetyTests.queue1")
private lazy var queue2 = DispatchQueue(label: "ThreadSafetyTests.queue2")
private lazy var queue3 = DispatchQueue(label: "ThreadSafetyTests.read")
override func setUp() {
super.setUp()
values = .init()
}
/// Without `@ThreadSafe` on this array, when modifying it we will
/// crash the app as both threads try to change the same memory address.
func testThreadSetOnMultipleThreads() {
let finish = expectation(description: "threads finish work")
finish.expectedFulfillmentCount = 3
func insert(value: Int) {
values.insert(value)
}
queue1.async {
for i in 0..<100 {
insert(value: i)
}
finish.fulfill()
}
queue2.async {
for i in 100..<200 {
insert(value: i)
}
finish.fulfill()
}
// this queue attempts to continuously read the value while the other threads
// are modifying the value. it demonstrates that the count value can be read concurrently
// while the other threads are modifying the value.
queue3.async {
for _ in 0..<200 {
let count = self.values.count
print("\(count)")
}
finish.fulfill()
}
waitForExpectations(timeout: 2, handler: nil)
var expectedValues = Set<Int>()
for i in 0..<200 {
expectedValues.insert(i)
}
XCTAssertEqual(values, expectedValues)
}
}
@gdavis
Copy link
Author

gdavis commented Dec 6, 2021

Updated with feedback from @chriseidhof to allow concurrent reading of the value while a modify is happening. For example, if we were to use the previous version of this wrapper using only NSLock to control access, and print out when the value is read in the property wrapper when the modify starts and stops, we'd get an output like this:

modify start
modify end
get value
modify start
modify end
modify start
modify end
modify start
modify end
modify start
modify end
modify start
modify end
modify start
modify end
modify start
modify end
modify start
modify end
get value
modify start
modify end
modify start
modify end
get value

You can see that the get value only prints inbetween modifications while the object is being locked. With this latest version that uses queue.sync { _value } when getting the current value, this changes so that reads can still occur as another thread modifies the value. Here's the output demonstrating this behavior:

modify start
modify end
modify start
get value
get value
get value
get value
get value
get value
modify end
get value
modify start
get value
modify end
get value
get value
modify start
get value
modify end
get value
modify start
get value
modify end
get value
modify start
get value
get value
modify end

Now, get value is able to be printed while another thread is within the modify property accessor and changing the value.

Here's where I added prints in the property wrapped to show this:

    public var wrappedValue: T {
        get {
            queue.sync {
                print("get value")
                return _value
            }
        }
        _modify {
            lock.lock()
            print("modify start")
            var tmp: T = _value

            defer {
                _value = tmp
                print("modify end")
                lock.unlock()
            }

            yield &tmp
        }
    }

@shahzadmajeed
Copy link

What is _modify and yield are these private apis in swift and are they safe for production/release code? I mean can we ship our code with these access modifiers?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment