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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What is
_modify
andyield
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?