├── .gitignore ├── .swiftformat ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── STS │ ├── ThreadSafe.swift │ └── UnfairLock.swift └── Tests └── STSTests ├── ModifyTests.swift ├── PerformanceTests.swift └── ReadWriteTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .swiftpm 92 | .DS_Store 93 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --self insert 2 | --wrapcollections before-first 3 | --wraparguments before-first 4 | --wrapparameters before-first 5 | --header strip 6 | --ifdef no-indent 7 | --enable wrapSwitchCases 8 | --enable organizeDeclarations 9 | --enable redundantOptionalBinding 10 | --swiftversion 5.8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Geor Kasapidi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "STS", 8 | products: [ 9 | // Products define the executables and libraries a package produces, making them visible to other packages. 10 | .library( 11 | name: "STS", 12 | targets: ["STS"] 13 | ), 14 | ], 15 | targets: [ 16 | // Targets are the basic building blocks of a package, defining a module or a test suite. 17 | // Targets can depend on other targets in this package and products from dependencies. 18 | .target( 19 | name: "STS"), 20 | .testTarget( 21 | name: "STSTests", 22 | dependencies: ["STS"] 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STS 2 | 3 | Convenient wrapper around os_unfair_lock and property warpper for atomic memory access on any apple platform. 4 | 5 | ``` swift 6 | let lock = UnfairLock() 7 | lock.lock() 8 | // ... 9 | lock.unlock() 10 | ``` 11 | 12 | ``` swift 13 | @ThreadSafe 14 | var value = SomeType() 15 | 16 | value.info.id += 1 // safe and atomic 17 | ``` 18 | -------------------------------------------------------------------------------- /Sources/STS/ThreadSafe.swift: -------------------------------------------------------------------------------- 1 | @propertyWrapper 2 | public final class ThreadSafe { 3 | // MARK: Lifecycle 4 | 5 | public init(wrappedValue: T) { 6 | self.value = wrappedValue 7 | } 8 | 9 | // MARK: Public 10 | 11 | public var projectedValue: ThreadSafe { self } 12 | 13 | public var wrappedValue: T { 14 | get { 15 | self.lock.lock(); defer { self.lock.unlock() } 16 | return self.value 17 | } 18 | _modify { 19 | self.lock.lock(); defer { self.lock.unlock() } 20 | yield &self.value 21 | } 22 | } 23 | 24 | public func read(_ f: (T) -> V) -> V { 25 | self.lock.lock(); defer { self.lock.unlock() } 26 | return f(self.value) 27 | } 28 | 29 | @discardableResult 30 | public func write(_ f: (inout T) -> V) -> V { 31 | self.lock.lock(); defer { self.lock.unlock() } 32 | return f(&self.value) 33 | } 34 | 35 | // MARK: Private 36 | 37 | private let lock = UnfairLock() 38 | 39 | private var value: T 40 | } 41 | -------------------------------------------------------------------------------- /Sources/STS/UnfairLock.swift: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | public final class UnfairLock { 4 | // MARK: Lifecycle 5 | 6 | public init() { 7 | self.pointer = .allocate(capacity: 1) 8 | self.pointer.initialize(to: os_unfair_lock()) 9 | } 10 | 11 | deinit { 12 | self.pointer.deinitialize(count: 1) 13 | self.pointer.deallocate() 14 | } 15 | 16 | // MARK: Public 17 | 18 | public func lock() { 19 | os_unfair_lock_lock(self.pointer) 20 | } 21 | 22 | public func unlock() { 23 | os_unfair_lock_unlock(self.pointer) 24 | } 25 | 26 | public func tryLock() -> Bool { 27 | os_unfair_lock_trylock(self.pointer) 28 | } 29 | 30 | @discardableResult 31 | @inlinable 32 | public func execute(_ action: () -> T) -> T { 33 | self.lock(); defer { self.unlock() } 34 | return action() 35 | } 36 | 37 | @discardableResult 38 | @inlinable 39 | public func tryExecute(_ action: () throws -> T) throws -> T { 40 | try self.execute { Result(catching: action) }.get() 41 | } 42 | 43 | // MARK: Private 44 | 45 | private let pointer: os_unfair_lock_t 46 | } 47 | -------------------------------------------------------------------------------- /Tests/STSTests/ModifyTests.swift: -------------------------------------------------------------------------------- 1 | import STS 2 | import XCTest 3 | 4 | /// To catch data races remove @ThreadSafe property wrappers and enable thread sanitizer for tests in scheme editor 5 | final class ModifyTests: XCTestCase { 6 | // MARK: Internal 7 | 8 | override func setUp() { 9 | super.setUp() 10 | 11 | self.structValue = .init() 12 | self.classValue = .init() 13 | } 14 | 15 | func testStructReadWriteMeasure() { 16 | let initialValue = self.structValue 17 | 18 | // _read = 0.348 19 | // get = 0.319 20 | 21 | measure { 22 | (0 ..< 100_000).forEach { _ in 23 | if Bool.random() { 24 | self.structValue.x.a = .random(in: 0 ... 1000) 25 | } else { 26 | self.structValue.x.b = .random(in: 0 ... 1000) 27 | } 28 | 29 | self.structValue.c = self.structValue.x.b + self.structValue.x.a 30 | } 31 | } 32 | 33 | XCTAssertNotEqual(self.structValue, initialValue) 34 | } 35 | 36 | func testStructMultiThreadReadWrite() { 37 | let initialValue = self.structValue 38 | 39 | DispatchQueue.concurrentPerform(iterations: 10000) { _ in 40 | self.modifyComplexStruct() 41 | } 42 | 43 | XCTAssertNotEqual(self.structValue, initialValue) 44 | } 45 | 46 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 47 | func testStructMultiTaskReadWrite() async { 48 | let initialValue = self.structValue 49 | 50 | await withThrowingTaskGroup(of: Void.self) { group in 51 | for _ in 1 ..< 10000 { 52 | group.addTask { 53 | try await Task.sleep(nanoseconds: .random(in: 10_000_000 ..< 200_000_000)) 54 | return self.modifyComplexStruct() 55 | } 56 | } 57 | } 58 | 59 | XCTAssertNotEqual(self.structValue, initialValue) 60 | } 61 | 62 | func testClassReadWrite() { 63 | let initialValue = self.classValue 64 | 65 | DispatchQueue.concurrentPerform(iterations: 10000) { _ in 66 | _ = self.classValue.c 67 | _ = self.classValue.x.a 68 | _ = self.classValue.x.b 69 | 70 | if Bool.random() { 71 | self.classValue.c = .random(in: 0 ... 1000) 72 | } else { 73 | if Bool.random() { 74 | self.classValue.x.a = .random(in: 0 ... 1000) 75 | } else { 76 | self.classValue.x.b = .random(in: 0 ... 1000) 77 | } 78 | } 79 | 80 | self.classValue.c += 1 81 | self.classValue.x.b += 1 82 | self.classValue.x.a += 1 83 | } 84 | 85 | XCTAssertEqual(self.classValue, initialValue) 86 | } 87 | 88 | func testArrayReadWrite() { 89 | let initialValue = self.arrayValue 90 | 91 | DispatchQueue.concurrentPerform(iterations: 10000) { _ in 92 | _ = self.arrayValue 93 | 94 | if Bool.random() { 95 | self.arrayValue.removeAll { 96 | $0 % 2 == 0 97 | } 98 | } else { 99 | (0 ..< 20).forEach { _ in 100 | self.arrayValue.append(.random(in: 0 ... 10)) 101 | } 102 | } 103 | 104 | self.arrayValue = self.arrayValue.map { 105 | $0 * 2 106 | } 107 | } 108 | 109 | XCTAssertNotEqual(self.arrayValue, initialValue) 110 | } 111 | 112 | // MARK: Private 113 | 114 | private struct ComplexStruct: Equatable { 115 | struct InnerStruct: Equatable { 116 | var a: Int = .zero 117 | var b: Int = .zero 118 | } 119 | 120 | var c: Int = .zero 121 | var x: InnerStruct = .init() 122 | } 123 | 124 | private final class ComplexClass: Equatable { 125 | final class InnerClass: Equatable { 126 | @ThreadSafe 127 | var a: Int = .zero 128 | 129 | @ThreadSafe 130 | var b: Int = .zero 131 | 132 | static func == (lhs: InnerClass, rhs: InnerClass) -> Bool { 133 | lhs.a == rhs.a && lhs.b == rhs.b 134 | } 135 | } 136 | 137 | @ThreadSafe 138 | var c: Int = .zero 139 | 140 | var x: InnerClass = .init() 141 | 142 | static func == (lhs: ComplexClass, rhs: ComplexClass) -> Bool { 143 | lhs.c == rhs.c && lhs.x == rhs.x 144 | } 145 | } 146 | 147 | @ThreadSafe 148 | private var structValue = ComplexStruct() 149 | 150 | @ThreadSafe 151 | private var classValue = ComplexClass() 152 | 153 | @ThreadSafe 154 | private var arrayValue: [Int] = [] 155 | 156 | private func modifyComplexStruct() { 157 | _ = self.structValue.c 158 | _ = self.structValue.x.a 159 | _ = self.structValue.x.b 160 | 161 | self.structValue.x.a += 5 162 | 163 | if Bool.random() { 164 | self.structValue.c = .random(in: 0 ... 1000) 165 | } else { 166 | if Bool.random() { 167 | self.structValue.x.a = .random(in: 0 ... 1000) 168 | } else { 169 | self.structValue.x.b = .random(in: 0 ... 1000) 170 | } 171 | } 172 | 173 | self.structValue.c += 1 174 | self.structValue.x.b += 1 175 | self.structValue.x.a += 1 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Tests/STSTests/PerformanceTests.swift: -------------------------------------------------------------------------------- 1 | import STS 2 | import XCTest 3 | 4 | final class PerformanceTests: XCTestCase { 5 | // MARK: Internal 6 | 7 | func testSTS() { 8 | @ThreadSafe 9 | var value = ComplexStruct() 10 | 11 | let t1 = Date() 12 | 13 | for _ in 0 ..< Constants.iterations { 14 | _ = $value.read { $0 } 15 | 16 | $value.write { 17 | $0.modify() 18 | } 19 | } 20 | 21 | let t2 = Date() 22 | print("0️⃣", t2.timeIntervalSince(t1)) 23 | } 24 | 25 | func testGCD() { 26 | var value = ComplexStruct() 27 | 28 | let queue = DispatchQueue(label: "test_queue") 29 | 30 | let t1 = Date() 31 | 32 | for _ in 0 ..< Constants.iterations { 33 | _ = queue.sync { 34 | value 35 | } 36 | 37 | queue.async { 38 | value.modify() 39 | } 40 | } 41 | 42 | let t2 = Date() 43 | print("1️⃣", t2.timeIntervalSince(t1)) 44 | } 45 | 46 | func testActor() async { 47 | actor Value { 48 | private var value = ComplexStruct() 49 | 50 | @discardableResult 51 | func read() -> ComplexStruct { 52 | self.value 53 | } 54 | 55 | func update() { 56 | self.value.modify() 57 | } 58 | } 59 | 60 | let a = Value() 61 | 62 | let t1 = Date() 63 | 64 | for _ in 0 ..< Constants.iterations { 65 | _ = await a.read() 66 | await a.update() 67 | } 68 | 69 | let t2 = Date() 70 | print("2️⃣", t2.timeIntervalSince(t1)) 71 | } 72 | 73 | // MARK: Private 74 | 75 | private enum Constants { 76 | static let iterations = 100_000 77 | } 78 | 79 | private struct ComplexStruct: Equatable { 80 | struct InnerStruct: Equatable { 81 | var a: Int = .zero 82 | var b: Int = .zero 83 | } 84 | 85 | var c: Int = .zero 86 | var x: InnerStruct = .init() 87 | 88 | mutating func modify() { 89 | _ = self.c 90 | _ = self.x.a 91 | _ = self.x.b 92 | 93 | if Bool.random() { 94 | self.c = .random(in: 0 ... 1000) 95 | } else { 96 | if Bool.random() { 97 | self.x.a = .random(in: 0 ... 1000) 98 | } else { 99 | self.x.b = .random(in: 0 ... 1000) 100 | } 101 | } 102 | 103 | self.c += 1 104 | self.x.b += 1 105 | self.x.a += 1 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Tests/STSTests/ReadWriteTests.swift: -------------------------------------------------------------------------------- 1 | import STS 2 | import XCTest 3 | 4 | final class ReadWriteTests: XCTestCase { 5 | // MARK: Internal 6 | 7 | func testPropertyWrapperAPI() { 8 | let initialValue = ComplexStruct() 9 | 10 | let ts = ThreadSafe(wrappedValue: initialValue) 11 | 12 | DispatchQueue.concurrentPerform(iterations: 10000) { _ in 13 | _ = ts.read { $0 } 14 | 15 | ts.write { 16 | _ = $0.c 17 | _ = $0.x.a 18 | _ = $0.x.b 19 | 20 | if Bool.random() { 21 | $0.c = .random(in: 0 ... 1000) 22 | } else { 23 | if Bool.random() { 24 | $0.x.a = .random(in: 0 ... 1000) 25 | } else { 26 | $0.x.b = .random(in: 0 ... 1000) 27 | } 28 | } 29 | 30 | $0.c += 1 31 | $0.x.b += 1 32 | $0.x.a += 1 33 | } 34 | } 35 | 36 | XCTAssertNotEqual(ts.wrappedValue, initialValue) 37 | } 38 | 39 | func testLockThrowing() { 40 | struct Err: Swift.Error {} 41 | 42 | let lock = UnfairLock() 43 | 44 | lock.lock() 45 | // ... 46 | lock.unlock() 47 | 48 | XCTAssertThrowsError(try lock.tryExecute { throw Err() } as Int) 49 | XCTAssert((try? lock.tryExecute { 5 }) == 5) 50 | } 51 | 52 | // MARK: Private 53 | 54 | private struct ComplexStruct: Equatable { 55 | struct InnerStruct: Equatable { 56 | var a: Int = .zero 57 | var b: Int = .zero 58 | } 59 | 60 | var c: Int = .zero 61 | var x: InnerStruct = .init() 62 | } 63 | } 64 | --------------------------------------------------------------------------------