├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources ├── AllocatedLock.swift └── Mutex.swift ├── Tests ├── AllocatedLock+Unsafe.swift ├── AllocatedLockTests.swift ├── AllocatedLockXCTests.swift ├── MutexTests.swift └── MutexXCTests.swift └── docker-run-tests.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | xcode_16_3: 10 | runs-on: macos-15 11 | env: 12 | DEVELOPER_DIR: /Applications/Xcode_16.3.app/Contents/Developer 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Version 17 | run: swift --version 18 | - name: Build 19 | run: swift build --build-tests --enable-code-coverage 20 | - name: Test 21 | run: swift test --skip-build --enable-code-coverage 22 | - name: Gather code coverage 23 | run: xcrun llvm-cov export -format="lcov" .build/debug/MutexPackageTests.xctest/Contents/MacOS/MutexPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage_report.lcov 24 | - name: Upload Coverage 25 | uses: codecov/codecov-action@v4 26 | with: 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | files: ./coverage_report.lcov 29 | 30 | xcode_16_2: 31 | runs-on: macos-15 32 | env: 33 | DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | - name: Version 38 | run: swift --version 39 | - name: Build 40 | run: swift build --build-tests 41 | - name: Test 42 | run: swift test --skip-build 43 | 44 | xcode_15_4: 45 | runs-on: macos-14 46 | env: 47 | DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | - name: Version 52 | run: swift --version 53 | - name: Build 54 | run: swift build --build-tests 55 | - name: Test 56 | run: swift test 57 | 58 | xcode_15_2: 59 | runs-on: macos-14 60 | env: 61 | DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | - name: Version 66 | run: swift --version 67 | - name: Build 68 | run: swift build --build-tests 69 | - name: Test 70 | run: swift test 71 | 72 | linux_swift_6_1: 73 | runs-on: ubuntu-latest 74 | container: swift:6.1 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | - name: Version 79 | run: swift --version 80 | - name: Build 81 | run: swift build --build-tests 82 | - name: Test 83 | run: swift test --skip-build 84 | 85 | linux_swift_6_0: 86 | runs-on: ubuntu-latest 87 | container: swift:6.0.3 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v4 91 | - name: Version 92 | run: swift --version 93 | - name: Build 94 | run: swift build --build-tests 95 | - name: Test 96 | run: swift test --skip-build 97 | 98 | linux_swift_5_10: 99 | runs-on: ubuntu-latest 100 | container: swift:5.10 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v4 104 | - name: Version 105 | run: swift --version 106 | - name: Build 107 | run: swift build --build-tests 108 | - name: Test 109 | run: swift test --skip-build 110 | 111 | linux_swift_5_9: 112 | runs-on: ubuntu-latest 113 | container: swift:5.9 114 | steps: 115 | - name: Checkout 116 | uses: actions/checkout@v4 117 | - name: Version 118 | run: swift --version 119 | - name: Build 120 | run: swift build --build-tests 121 | - name: Test 122 | run: swift test --skip-build 123 | 124 | linux_swift_6_1_musl: 125 | runs-on: ubuntu-latest 126 | container: swift:6.1 127 | steps: 128 | - name: Checkout 129 | uses: actions/checkout@v4 130 | - name: Version 131 | run: swift --version 132 | - name: SDK List Pre 133 | run: swift sdk list 134 | - name: Install SDK 135 | run: swift sdk install https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum 111c6f7d280a651208b8c74c0521dd99365d785c1976a6e23162f55f65379ac6 136 | - name: SDK List Post 137 | run: swift sdk list 138 | - name: Build 139 | run: swift build --swift-sdk x86_64-swift-linux-musl 140 | 141 | windows_swift_6_1: 142 | runs-on: windows-latest 143 | steps: 144 | - name: Checkout 145 | uses: actions/checkout@v4 146 | - name: Install Swift 147 | uses: SwiftyLab/setup-swift@latest 148 | with: 149 | swift-version: "6.1.0" 150 | - name: Version 151 | run: swift --version 152 | - name: Build 153 | run: swift build --build-tests 154 | - name: Test 155 | run: swift test --skip-build 156 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Simon Whitty 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:6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Mutex", 7 | platforms: [ 8 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1) 9 | ], 10 | products: [ 11 | .library( 12 | name: "Mutex", 13 | targets: ["Mutex"] 14 | ) 15 | ], 16 | targets: [ 17 | .target( 18 | name: "Mutex", 19 | path: "Sources", 20 | swiftSettings: .upcomingFeatures 21 | ), 22 | .testTarget( 23 | name: "MutexTests", 24 | dependencies: ["Mutex"], 25 | path: "Tests", 26 | swiftSettings: .upcomingFeatures 27 | ) 28 | ] 29 | ) 30 | 31 | extension Array where Element == SwiftSetting { 32 | 33 | static var upcomingFeatures: [SwiftSetting] { 34 | [ 35 | .swiftLanguageMode(.v6) 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Mutex", 7 | platforms: [ 8 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) 9 | ], 10 | products: [ 11 | .library( 12 | name: "Mutex", 13 | targets: ["Mutex"] 14 | ) 15 | ], 16 | targets: [ 17 | .target( 18 | name: "Mutex", 19 | path: "Sources", 20 | swiftSettings: .upcomingFeatures 21 | ), 22 | .testTarget( 23 | name: "MutexTests", 24 | dependencies: ["Mutex"], 25 | path: "Tests", 26 | swiftSettings: .upcomingFeatures 27 | ) 28 | ] 29 | ) 30 | 31 | extension Array where Element == SwiftSetting { 32 | 33 | static var upcomingFeatures: [SwiftSetting] { 34 | [ 35 | .enableExperimentalFeature("StrictConcurrency") 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/swhitty/swift-mutex/actions/workflows/build.yml/badge.svg)](https://github.com/swhitty/swift-mutex/actions/workflows/build.yml) 2 | [![Codecov](https://codecov.io/gh/swhitty/swift-mutex/graphs/badge.svg)](https://codecov.io/gh/swhitty/swift-mutex) 3 | [![Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswhitty%2Fswift-mutex%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swhitty/swift-mutex) 4 | [![Swift 6.0](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswhitty%2Fswift-mutex%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swhitty/swift-mutex) 5 | 6 | # Introduction 7 | 8 | **swift-mutex** is a cross platform lock backporting the Swift 6 [`Mutex`](https://developer.apple.com/documentation/synchronization/mutex) API to Swift 5.9 and all Darwin platforms. 9 | 10 | Mutex is built upon `AllocatedLock`, a cross platform lock with an API compatible with [`OSAllocatedUnfairLock`](https://developer.apple.com/documentation/os/osallocatedunfairlock). The lock wraps [`os_unfair_lock_t`](https://developer.apple.com/documentation/os/os_unfair_lock_t) on Darwin platforms, [`pthread_mutex_t`](https://man.freebsd.org/cgi/man.cgi?pthread_mutex_lock(3)) on Linux and [`SRWLOCK`](https://learn.microsoft.com/en-us/windows/win32/sync/slim-reader-writer--srw--locks) on Windows. 11 | 12 | # Installation 13 | 14 | The package can be installed by using Swift Package Manager. 15 | 16 | **Note:** Mutex requires Swift 5.9 on Xcode 15+. It runs on iOS 13+, tvOS 13+, macOS 10.15+, Linux and Windows. 17 | To install using Swift Package Manager, add this to the `dependencies:` section in your Package.swift file: 18 | 19 | ```swift 20 | .package(url: "https://github.com/swhitty/swift-mutex.git", .upToNextMajor(from: "0.0.5")) 21 | ``` 22 | 23 | # Usage 24 | 25 | Usage is similar to the Swift 6 [`Mutex`](https://developer.apple.com/documentation/synchronization/mutex) 26 | 27 | ```swift 28 | let state = Mutex(0) 29 | ``` 30 | 31 | Use `.withLock` to acquire the lock to read the state: 32 | ```swift 33 | let val = state.withLock { $0 } 34 | ``` 35 | 36 | Or mutate the state: 37 | ```swift 38 | let val = state.withLock { 39 | $0 += 1 40 | return $0 41 | } 42 | ``` 43 | 44 | # Gist 45 | 46 | A simpler, single file version compatible with macOS 13 / iOS 16 can be found in [this gist](https://gist.github.com/swhitty/571deb25d84c1954a7a01aafa661496e). 47 | 48 | 49 | # Credits 50 | 51 | swift-mutex is primarily the work of [Simon Whitty](https://github.com/swhitty). 52 | 53 | ([Full list of contributors](https://github.com/swhitty/swift-mutex/graphs/contributors)) 54 | -------------------------------------------------------------------------------- /Sources/AllocatedLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllocatedLock.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 10/04/2023. 6 | // Copyright 2023 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | // Backports the Swift interface around OSAllocatedUnfairLock available in recent Darwin platforms 33 | public struct AllocatedLock: @unchecked Sendable { 34 | 35 | @usableFromInline 36 | let storage: Storage 37 | 38 | public init(uncheckedState initialState: State) { 39 | self.storage = Storage(initialState: initialState) 40 | } 41 | 42 | public init(initialState: State) where State: Sendable { 43 | self.storage = Storage(initialState: initialState) 44 | } 45 | 46 | @inlinable 47 | public func withLock(_ body: @Sendable (inout State) throws -> R) rethrows -> R where R: Sendable { 48 | storage.lock() 49 | defer { storage.unlock() } 50 | return try body(&storage.state) 51 | } 52 | 53 | @inlinable 54 | public func withLockIfAvailable(_ body: @Sendable (inout State) throws -> R) rethrows -> R? where R: Sendable { 55 | guard storage.tryLock() else { return nil } 56 | defer { storage.unlock() } 57 | return try body(&storage.state) 58 | } 59 | 60 | @inlinable 61 | public func withLockIfAvailableUnchecked(_ body: (inout State) throws -> R) rethrows -> R? { 62 | guard storage.tryLock() else { return nil } 63 | defer { storage.unlock() } 64 | return try body(&storage.state) 65 | } 66 | 67 | @inlinable 68 | public func withLockUnchecked(_ body: (inout State) throws -> R) rethrows -> R { 69 | storage.lock() 70 | defer { storage.unlock() } 71 | return try body(&storage.state) 72 | } 73 | 74 | } 75 | 76 | public extension AllocatedLock where State == Void { 77 | 78 | init() { 79 | self.storage = Storage(initialState: ()) 80 | } 81 | 82 | @inlinable @available(*, noasync) 83 | func lock() { 84 | storage.lock() 85 | } 86 | 87 | @inlinable @available(*, noasync) 88 | func lockIfAvailable() -> Bool { 89 | storage.tryLock() 90 | } 91 | 92 | @inlinable @available(*, noasync) 93 | func unlock() { 94 | storage.unlock() 95 | } 96 | 97 | @inlinable 98 | func withLock(_ body: @Sendable () throws -> R) rethrows -> R where R: Sendable { 99 | storage.lock() 100 | defer { storage.unlock() } 101 | return try body() 102 | } 103 | 104 | @inlinable 105 | func withLockIfAvailable(_ body: @Sendable () throws -> R) rethrows -> R? where R: Sendable { 106 | guard storage.tryLock() else { return nil } 107 | defer { storage.unlock() } 108 | return try body() 109 | } 110 | 111 | @inlinable 112 | func withLockIfAvailableUnchecked(_ body: () throws -> R) rethrows -> R? { 113 | guard storage.tryLock() else { return nil } 114 | defer { storage.unlock() } 115 | return try body() 116 | } 117 | 118 | @inlinable 119 | func withLockUnchecked(_ body: () throws -> R) rethrows -> R { 120 | storage.lock() 121 | defer { storage.unlock() } 122 | return try body() 123 | } 124 | } 125 | 126 | #if canImport(Darwin) 127 | 128 | import struct os.os_unfair_lock_t 129 | import struct os.os_unfair_lock 130 | import func os.os_unfair_lock_lock 131 | import func os.os_unfair_lock_unlock 132 | import func os.os_unfair_lock_trylock 133 | 134 | extension AllocatedLock { 135 | @usableFromInline 136 | final class Storage { 137 | private let _lock: os_unfair_lock_t 138 | 139 | @usableFromInline 140 | var state: State 141 | 142 | init(initialState: State) { 143 | self._lock = .allocate(capacity: 1) 144 | self._lock.initialize(to: os_unfair_lock()) 145 | self.state = initialState 146 | } 147 | 148 | @usableFromInline 149 | func lock() { 150 | os_unfair_lock_lock(_lock) 151 | } 152 | 153 | @usableFromInline 154 | func unlock() { 155 | os_unfair_lock_unlock(_lock) 156 | } 157 | 158 | @usableFromInline 159 | func tryLock() -> Bool { 160 | os_unfair_lock_trylock(_lock) 161 | } 162 | 163 | deinit { 164 | self._lock.deinitialize(count: 1) 165 | self._lock.deallocate() 166 | } 167 | } 168 | } 169 | 170 | #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) 171 | 172 | #if canImport(Musl) 173 | import Musl 174 | #elseif canImport(Glibc) 175 | import Glibc 176 | #elseif canImport(Bionic) 177 | import Bionic 178 | #endif 179 | 180 | extension AllocatedLock { 181 | @usableFromInline 182 | final class Storage { 183 | private let _lock: UnsafeMutablePointer 184 | 185 | @usableFromInline 186 | var state: State 187 | 188 | init(initialState: State) { 189 | var attr = pthread_mutexattr_t() 190 | pthread_mutexattr_init(&attr) 191 | self._lock = .allocate(capacity: 1) 192 | let err = pthread_mutex_init(self._lock, &attr) 193 | precondition(err == 0, "pthread_mutex_init error: \(err)") 194 | self.state = initialState 195 | } 196 | 197 | @usableFromInline 198 | func lock() { 199 | let err = pthread_mutex_lock(_lock) 200 | precondition(err == 0, "pthread_mutex_lock error: \(err)") 201 | } 202 | 203 | @usableFromInline 204 | func unlock() { 205 | let err = pthread_mutex_unlock(_lock) 206 | precondition(err == 0, "pthread_mutex_unlock error: \(err)") 207 | } 208 | 209 | @usableFromInline 210 | func tryLock() -> Bool { 211 | pthread_mutex_trylock(_lock) == 0 212 | } 213 | 214 | deinit { 215 | let err = pthread_mutex_destroy(self._lock) 216 | precondition(err == 0, "pthread_mutex_destroy error: \(err)") 217 | self._lock.deallocate() 218 | } 219 | } 220 | } 221 | 222 | #elseif canImport(WinSDK) 223 | 224 | import ucrt 225 | import WinSDK 226 | 227 | extension AllocatedLock { 228 | @usableFromInline 229 | final class Storage { 230 | private let _lock: UnsafeMutablePointer 231 | 232 | @usableFromInline 233 | var state: State 234 | 235 | init(initialState: State) { 236 | self._lock = .allocate(capacity: 1) 237 | InitializeSRWLock(self._lock) 238 | self.state = initialState 239 | } 240 | 241 | @usableFromInline 242 | func lock() { 243 | AcquireSRWLockExclusive(_lock) 244 | } 245 | 246 | @usableFromInline 247 | func unlock() { 248 | ReleaseSRWLockExclusive(_lock) 249 | } 250 | 251 | @usableFromInline 252 | func tryLock() -> Bool { 253 | TryAcquireSRWLockExclusive(_lock) != 0 254 | } 255 | } 256 | } 257 | 258 | #endif 259 | -------------------------------------------------------------------------------- /Sources/Mutex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mutex.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 07/09/2024. 6 | // Copyright 2024 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | // Backports the Swift 6 type Mutex to all Darwin platforms 33 | 34 | // @available(macOS, deprecated: 15.0, message: "use Mutex from Synchronization module included with Swift 6") 35 | // @available(iOS, deprecated: 18.0, message: "use Mutex from Synchronization module included with Swift 6") 36 | // @available(tvOS, deprecated: 18.0, message: "use Mutex from Synchronization module included with Swift 6") 37 | // @available(watchOS, deprecated: 11.0, message: "use Mutex from Synchronization module included with Swift 6") 38 | // @available(visionOS, deprecated: 2.0, message: "use Mutex from Synchronization module included with Swift 6") 39 | public struct Mutex: Sendable { 40 | let lock: AllocatedLock // Compatible with OSAllocatedUnfairLock iOS 16+ 41 | } 42 | 43 | #if compiler(>=6) 44 | public extension Mutex { 45 | init(_ initialValue: consuming sending Value) { 46 | self.lock = AllocatedLock(uncheckedState: initialValue) 47 | } 48 | 49 | borrowing func withLock( 50 | _ body: (inout sending Value) throws(E) -> sending Result 51 | ) throws(E) -> sending Result { 52 | do { 53 | return try lock.withLockUnchecked { value in 54 | nonisolated(unsafe) var copy = value 55 | defer { value = copy } 56 | return try Transferring(body(©)) 57 | }.value 58 | } catch let error as E { 59 | throw error 60 | } catch { 61 | preconditionFailure("cannot occur") 62 | } 63 | } 64 | 65 | borrowing func withLockIfAvailable( 66 | _ body: (inout sending Value) throws(E) -> sending Result 67 | ) throws(E) -> sending Result? where E: Error { 68 | do { 69 | return try lock.withLockIfAvailableUnchecked { value in 70 | nonisolated(unsafe) var copy = value 71 | defer { value = copy } 72 | return try Transferring(body(©)) 73 | }?.value 74 | } catch let error as E { 75 | throw error 76 | } catch { 77 | preconditionFailure("cannot occur") 78 | } 79 | } 80 | } 81 | private struct Transferring { 82 | nonisolated(unsafe) var value: T 83 | 84 | init(_ value: T) { 85 | self.value = value 86 | } 87 | } 88 | #else 89 | public extension Mutex { 90 | init(_ initialValue: consuming Value) { 91 | self.lock = AllocatedLock(uncheckedState: initialValue) 92 | } 93 | 94 | borrowing func withLock( 95 | _ body: (inout Value) throws -> Result 96 | ) rethrows -> Result { 97 | try lock.withLockUnchecked { 98 | return try body(&$0) 99 | } 100 | } 101 | 102 | borrowing func withLockIfAvailable( 103 | _ body: (inout Value) throws -> Result 104 | ) rethrows -> Result? { 105 | try lock.withLockIfAvailableUnchecked { 106 | return try body(&$0) 107 | } 108 | } 109 | } 110 | #endif 111 | -------------------------------------------------------------------------------- /Tests/AllocatedLock+Unsafe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllocatedLock+Unsafe.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 11/02/2025. 6 | // Copyright 2025 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | @testable import Mutex 33 | 34 | // sidestep warning: unavailable from asynchronous contexts 35 | extension AllocatedLock { 36 | func unsafeLock() { storage.lock() } 37 | func unsafeUnlock() { storage.unlock() } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/AllocatedLockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllocatedLockTests.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 10/04/2023. 6 | // Copyright 2023 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Testing) 33 | @testable import Mutex 34 | import Testing 35 | 36 | struct AllocatedLockTests { 37 | 38 | @Test 39 | func lockState_IsProtected() async { 40 | let state = AllocatedLock(initialState: 0) 41 | 42 | let total = await withTaskGroup(of: Void.self) { group in 43 | for i in 1...1000 { 44 | group.addTask { 45 | state.withLock { $0 += i } 46 | } 47 | } 48 | await group.waitForAll() 49 | return state.withLock { $0 } 50 | } 51 | 52 | #expect(total == 500500) 53 | } 54 | 55 | @Test 56 | func lock_ReturnsValue() async { 57 | let lock = AllocatedLock() 58 | let value = lock.withLock { true } 59 | #expect(value) 60 | } 61 | 62 | @Test 63 | func lock_Blocks() async { 64 | let lock = AllocatedLock() 65 | await MainActor.run { 66 | lock.unsafeLock() 67 | } 68 | 69 | Task { @MainActor in 70 | try? await Task.sleep(nanoseconds: 200_000) 71 | lock.unsafeUnlock() 72 | } 73 | 74 | let results = await withTaskGroup(of: Bool.self) { group in 75 | group.addTask { 76 | try? await Task.sleep(nanoseconds: 10_000) 77 | return true 78 | } 79 | group.addTask { 80 | lock.unsafeLock() 81 | lock.unsafeUnlock() 82 | return false 83 | } 84 | let first = await group.next()! 85 | let second = await group.next()! 86 | return [first, second] 87 | } 88 | #expect(results == [true, false]) 89 | } 90 | 91 | @Test 92 | func tryLock() { 93 | let lock = AllocatedLock() 94 | let value = lock.withLock { true } 95 | #expect(value) 96 | } 97 | 98 | @Test 99 | func ifAvailable() { 100 | let lock = AllocatedLock(uncheckedState: 5) 101 | #expect( 102 | lock.withLock { _ in "fish" } == "fish" 103 | ) 104 | 105 | lock.unsafeLock() 106 | #expect( 107 | lock.withLockIfAvailable { _ in "fish" } == nil 108 | ) 109 | 110 | lock.unsafeUnlock() 111 | #expect( 112 | lock.withLockIfAvailable { _ in "fish" } == "fish" 113 | ) 114 | } 115 | 116 | @Test 117 | func ifAvailableUnchecked() { 118 | let lock = AllocatedLock(uncheckedState: NonSendable("fish")) 119 | #expect( 120 | lock.withLockUnchecked { $0 }.name == "fish" 121 | ) 122 | 123 | lock.unsafeLock() 124 | #expect( 125 | lock.withLockIfAvailableUnchecked { $0 }?.name == nil 126 | ) 127 | 128 | lock.unsafeUnlock() 129 | #expect( 130 | lock.withLockIfAvailableUnchecked { $0 }?.name == "fish" 131 | ) 132 | } 133 | 134 | @Test 135 | func voidIfAvailable() { 136 | let lock = AllocatedLock() 137 | #expect( 138 | lock.withLock { "fish" } == "fish" 139 | ) 140 | 141 | lock.unsafeLock() 142 | #expect( 143 | lock.withLockIfAvailable { "fish" } == nil 144 | ) 145 | 146 | lock.unsafeUnlock() 147 | #expect( 148 | lock.withLockIfAvailable { "fish" } == "fish" 149 | ) 150 | } 151 | 152 | @Test 153 | func voidIfAvailableUnchecked() { 154 | let lock = AllocatedLock() 155 | #expect( 156 | lock.withLockUnchecked { NonSendable("fish") }.name == "fish" 157 | ) 158 | 159 | lock.lock() 160 | #expect( 161 | lock.withLockIfAvailableUnchecked { NonSendable("fish") } == nil 162 | ) 163 | 164 | lock.unlock() 165 | #expect( 166 | lock.withLockIfAvailableUnchecked { NonSendable("chips") }?.name == "chips" 167 | ) 168 | } 169 | 170 | @Test 171 | func voidLock() { 172 | let lock = AllocatedLock() 173 | lock.lock() 174 | #expect(lock.lockIfAvailable() == false) 175 | lock.unlock() 176 | #expect(lock.lockIfAvailable()) 177 | lock.unlock() 178 | } 179 | } 180 | 181 | public final class NonSendable { 182 | 183 | let name: String 184 | 185 | init(_ name: String) { 186 | self.name = name 187 | } 188 | } 189 | #endif 190 | -------------------------------------------------------------------------------- /Tests/AllocatedLockXCTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllocatedLockXCTests.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 10/04/2023. 6 | // Copyright 2023 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if !canImport(Testing) 33 | @testable import Mutex 34 | import XCTest 35 | 36 | final class AllocatedLockTests: XCTestCase { 37 | 38 | func testLockState_IsProtected() async { 39 | let state = AllocatedLock(initialState: 0) 40 | 41 | let total = await withTaskGroup(of: Void.self) { group in 42 | for i in 1...1000 { 43 | group.addTask { 44 | state.withLock { $0 += i } 45 | } 46 | } 47 | await group.waitForAll() 48 | return state.withLock { $0 } 49 | } 50 | 51 | XCTAssertEqual(total, 500500) 52 | } 53 | 54 | func testLock_ReturnsValue() async { 55 | let lock = AllocatedLock() 56 | let value = lock.withLock { true } 57 | XCTAssertTrue(value) 58 | } 59 | 60 | 61 | func testLock_Blocks() async { 62 | let lock = AllocatedLock() 63 | await MainActor.run { 64 | lock.unsafeLock() 65 | } 66 | 67 | Task { @MainActor in 68 | try? await Task.sleep(nanoseconds: 200_000) 69 | lock.unsafeUnlock() 70 | } 71 | 72 | let results = await withTaskGroup(of: Bool.self) { group in 73 | group.addTask { 74 | try? await Task.sleep(nanoseconds: 10_000) 75 | return true 76 | } 77 | group.addTask { 78 | lock.unsafeLock() 79 | lock.unsafeUnlock() 80 | return false 81 | } 82 | let first = await group.next()! 83 | let second = await group.next()! 84 | return [first, second] 85 | } 86 | XCTAssertEqual(results, [true, false]) 87 | } 88 | 89 | func testTryLock() { 90 | let lock = AllocatedLock() 91 | let value = lock.withLock { true } 92 | XCTAssertTrue(value) 93 | } 94 | 95 | func testIfAvailable() { 96 | let lock = AllocatedLock(uncheckedState: 5) 97 | XCTAssertEqual( 98 | lock.withLock { _ in "fish" }, 99 | "fish" 100 | ) 101 | 102 | lock.unsafeLock() 103 | XCTAssertEqual( 104 | lock.withLockIfAvailable { _ in "fish" }, 105 | String?.none 106 | ) 107 | 108 | lock.unsafeUnlock() 109 | XCTAssertEqual( 110 | lock.withLockIfAvailable { _ in "fish" }, 111 | "fish" 112 | ) 113 | } 114 | 115 | func testIfAvailableUnchecked() { 116 | let lock = AllocatedLock(uncheckedState: NonSendable("fish")) 117 | XCTAssertEqual( 118 | lock.withLockUnchecked { $0 }.name, 119 | "fish" 120 | ) 121 | 122 | lock.unsafeLock() 123 | XCTAssertNil( 124 | lock.withLockIfAvailableUnchecked { $0 }?.name 125 | ) 126 | 127 | lock.unsafeUnlock() 128 | XCTAssertEqual( 129 | lock.withLockIfAvailableUnchecked { $0 }?.name, 130 | "fish" 131 | ) 132 | } 133 | 134 | func testVoidIfAvailable() { 135 | let lock = AllocatedLock() 136 | XCTAssertEqual( 137 | lock.withLock { "fish" }, 138 | "fish" 139 | ) 140 | 141 | lock.unsafeLock() 142 | XCTAssertEqual( 143 | lock.withLockIfAvailable { "fish" }, 144 | String?.none 145 | ) 146 | 147 | lock.unsafeUnlock() 148 | XCTAssertEqual( 149 | lock.withLockIfAvailable { "fish" }, 150 | "fish" 151 | ) 152 | } 153 | 154 | func testVoidIfAvailableUnchecked() { 155 | let lock = AllocatedLock() 156 | XCTAssertEqual( 157 | lock.withLockUnchecked { NonSendable("fish") }.name, 158 | "fish" 159 | ) 160 | 161 | lock.lock() 162 | XCTAssertNil( 163 | lock.withLockIfAvailableUnchecked { NonSendable("fish") } 164 | ) 165 | 166 | lock.unlock() 167 | XCTAssertEqual( 168 | lock.withLockIfAvailableUnchecked { NonSendable("chips") }?.name, 169 | "chips" 170 | ) 171 | } 172 | 173 | func testVoidLock() { 174 | let lock = AllocatedLock() 175 | lock.lock() 176 | XCTAssertFalse(lock.lockIfAvailable()) 177 | lock.unlock() 178 | XCTAssertTrue(lock.lockIfAvailable()) 179 | lock.unlock() 180 | } 181 | } 182 | 183 | public final class NonSendable { 184 | 185 | let name: String 186 | 187 | init(_ name: String) { 188 | self.name = name 189 | } 190 | } 191 | #endif 192 | -------------------------------------------------------------------------------- /Tests/MutexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutexTests.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 07/09/2024. 6 | // Copyright 2024 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if canImport(Testing) 33 | @testable import Mutex 34 | import Testing 35 | 36 | struct MutexTests { 37 | 38 | @Test 39 | func withLock_ReturnsValue() { 40 | let mutex = Mutex("fish") 41 | let val = mutex.withLock { 42 | $0 + " & chips" 43 | } 44 | #expect(val == "fish & chips") 45 | } 46 | 47 | @Test 48 | func withLock_ThrowsError() { 49 | let mutex = Mutex("fish") 50 | #expect(throws: CancellationError.self) { 51 | try mutex.withLock { _ -> Void in throw CancellationError() } 52 | } 53 | } 54 | 55 | @Test 56 | func lockIfAvailable_ReturnsValue() { 57 | let mutex = Mutex("fish") 58 | mutex.lock.unsafeLock() 59 | #expect( 60 | mutex.withLockIfAvailable { _ in "chips" } == nil 61 | ) 62 | mutex.lock.unsafeUnlock() 63 | #expect( 64 | mutex.withLockIfAvailable { _ in "chips" } == "chips" 65 | ) 66 | } 67 | 68 | @Test 69 | func withLockIfAvailable_ThrowsError() { 70 | let mutex = Mutex("fish") 71 | #expect(throws: CancellationError.self) { 72 | try mutex.withLockIfAvailable { _ -> Void in throw CancellationError() } 73 | } 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /Tests/MutexXCTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutexXCTests.swift 3 | // swift-mutex 4 | // 5 | // Created by Simon Whitty on 07/09/2024. 6 | // Copyright 2024 Simon Whitty 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/swhitty/swift-mutex 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #if !canImport(Testing) 33 | @testable import Mutex 34 | import XCTest 35 | 36 | final class MutexTests: XCTestCase { 37 | 38 | func testWithLock_ReturnsValue() { 39 | let mutex = Mutex("fish") 40 | let val = mutex.withLock { 41 | $0 + " & chips" 42 | } 43 | XCTAssertEqual(val, "fish & chips") 44 | } 45 | 46 | func testWithLock_ThrowsError() { 47 | let mutex = Mutex("fish") 48 | XCTAssertThrowsError(try mutex.withLock { _ -> Void in throw CancellationError() }) { 49 | _ = $0 is CancellationError 50 | } 51 | } 52 | 53 | func testLockIfAvailable_ReturnsValue() { 54 | let mutex = Mutex("fish") 55 | mutex.lock.unsafeLock() 56 | XCTAssertNil( 57 | mutex.withLockIfAvailable { _ in "chips" } 58 | ) 59 | mutex.lock.unsafeUnlock() 60 | XCTAssertEqual( 61 | mutex.withLockIfAvailable { _ in "chips" }, 62 | "chips" 63 | ) 64 | } 65 | 66 | func testWithLockIfAvailable_ThrowsError() { 67 | let mutex = Mutex("fish") 68 | XCTAssertThrowsError(try mutex.withLockIfAvailable { _ -> Void in throw CancellationError() }) { 69 | _ = $0 is CancellationError 70 | } 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /docker-run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | docker run -it \ 6 | --rm \ 7 | --mount src="$(pwd)",target=/mutex,type=bind \ 8 | swiftlang/swift:nightly-6.0-jammy \ 9 | /usr/bin/swift test --package-path /mutex 10 | --------------------------------------------------------------------------------