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)
}
}
@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