Last active
October 21, 2024 13:49
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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) | |
} | |
} |
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
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:You can see that the
get value
only prints inbetween modifications while the object is being locked. With this latest version that usesqueue.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: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: