Skip to content

Commit 5831aa9

Browse files
committed
Mutex
1 parent 243bbb8 commit 5831aa9

File tree

5 files changed

+363
-13
lines changed

5 files changed

+363
-13
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageDescription
55
let package = Package(
66
name: "Timeout",
77
platforms: [
8-
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)
8+
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)
99
],
1010
products: [
1111
.library(

Sources/Mutex.swift

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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.0 Mutex API
33+
@usableFromInline
34+
struct Mutex<Value>: @unchecked Sendable {
35+
let storage: Storage
36+
}
37+
38+
#if compiler(>=6)
39+
extension Mutex {
40+
41+
@usableFromInline
42+
init(_ initialValue: consuming sending Value) {
43+
self.storage = Storage(initialValue)
44+
}
45+
46+
@usableFromInline
47+
borrowing func withLock<Result, E: Error>(
48+
_ body: (inout sending Value) throws(E) -> sending Result
49+
) throws(E) -> sending Result {
50+
storage.lock()
51+
defer { storage.unlock() }
52+
return try body(&storage.value)
53+
}
54+
55+
@usableFromInline
56+
borrowing func withLockIfAvailable<Result, E>(
57+
_ body: (inout sending Value) throws(E) -> sending Result
58+
) throws(E) -> sending Result? where E: Error {
59+
guard storage.tryLock() else { return nil }
60+
defer { storage.unlock() }
61+
return try body(&storage.value)
62+
}
63+
}
64+
#else
65+
extension Mutex {
66+
67+
@usableFromInline
68+
init(_ initialValue: Value) {
69+
self.storage = Storage(initialValue)
70+
}
71+
72+
@usableFromInline
73+
borrowing func withLock<Result>(
74+
_ body: (inout Value) throws -> Result
75+
) rethrows -> Result {
76+
storage.lock()
77+
defer { storage.unlock() }
78+
return try body(&storage.value)
79+
}
80+
81+
@usableFromInline
82+
borrowing func withLockIfAvailable<Result>(
83+
_ body: (inout Value) throws -> Result
84+
) rethrows -> Result? {
85+
guard storage.tryLock() else { return nil }
86+
defer { storage.unlock() }
87+
return try body(&storage.value)
88+
}
89+
}
90+
#endif
91+
92+
#if canImport(Darwin)
93+
94+
import struct os.os_unfair_lock_t
95+
import struct os.os_unfair_lock
96+
import func os.os_unfair_lock_lock
97+
import func os.os_unfair_lock_unlock
98+
import func os.os_unfair_lock_trylock
99+
100+
extension Mutex {
101+
102+
final class Storage {
103+
private let _lock: os_unfair_lock_t
104+
105+
var value: Value
106+
107+
init(_ initialValue: Value) {
108+
self._lock = .allocate(capacity: 1)
109+
self._lock.initialize(to: os_unfair_lock())
110+
self.value = initialValue
111+
}
112+
113+
func lock() {
114+
os_unfair_lock_lock(_lock)
115+
}
116+
117+
func unlock() {
118+
os_unfair_lock_unlock(_lock)
119+
}
120+
121+
func tryLock() -> Bool {
122+
os_unfair_lock_trylock(_lock)
123+
}
124+
125+
deinit {
126+
self._lock.deinitialize(count: 1)
127+
self._lock.deallocate()
128+
}
129+
}
130+
}
131+
132+
#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic)
133+
134+
#if canImport(Musl)
135+
import Musl
136+
#elseif canImport(Bionic)
137+
import Android
138+
#else
139+
import Glibc
140+
#endif
141+
142+
extension Mutex {
143+
144+
final class Storage {
145+
private let _lock: UnsafeMutablePointer<pthread_mutex_t>
146+
147+
var value: Value
148+
149+
init(_ initialValue: Value) {
150+
var attr = pthread_mutexattr_t()
151+
pthread_mutexattr_init(&attr)
152+
self._lock = .allocate(capacity: 1)
153+
let err = pthread_mutex_init(self._lock, &attr)
154+
precondition(err == 0, "pthread_mutex_init error: \(err)")
155+
self.value = initialValue
156+
}
157+
158+
func lock() {
159+
let err = pthread_mutex_lock(_lock)
160+
precondition(err == 0, "pthread_mutex_lock error: \(err)")
161+
}
162+
163+
func unlock() {
164+
let err = pthread_mutex_unlock(_lock)
165+
precondition(err == 0, "pthread_mutex_unlock error: \(err)")
166+
}
167+
168+
func tryLock() -> Bool {
169+
pthread_mutex_trylock(_lock) == 0
170+
}
171+
172+
deinit {
173+
let err = pthread_mutex_destroy(self._lock)
174+
precondition(err == 0, "pthread_mutex_destroy error: \(err)")
175+
self._lock.deallocate()
176+
}
177+
}
178+
}
179+
180+
#elseif canImport(WinSDK)
181+
182+
import ucrt
183+
import WinSDK
184+
185+
extension Mutex {
186+
187+
final class Storage {
188+
private let _lock: UnsafeMutablePointer<SRWLOCK>
189+
190+
var value: Value
191+
192+
init(_ initialValue: Value) {
193+
self._lock = .allocate(capacity: 1)
194+
InitializeSRWLock(self._lock)
195+
self.value = initialValue
196+
}
197+
198+
func lock() {
199+
AcquireSRWLockExclusive(_lock)
200+
}
201+
202+
func unlock() {
203+
ReleaseSRWLockExclusive(_lock)
204+
}
205+
206+
func tryLock() -> Bool {
207+
TryAcquireSRWLockExclusive(_lock) != 0
208+
}
209+
}
210+
}
211+
212+
#endif

Sources/withThrowingTimeout.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,8 @@ private func _withThrowingTimeout<T>(
109109
}
110110
}
111111

112-
private struct Transferring<Value>: Sendable {
113-
nonisolated(unsafe) public var value: Value
114-
init(_ value: Value) {
115-
self.value = value
116-
}
117-
}
118112
#else
113+
119114
public func withThrowingTimeout<T>(
120115
seconds: TimeInterval,
121116
body: () async throws -> T
@@ -178,10 +173,4 @@ private func _withThrowingTimeout<T: Sendable>(
178173
}
179174
}
180175

181-
private struct Transferring<Value>: @unchecked Sendable {
182-
var value: Value
183-
init(_ value: Value) {
184-
self.value = value
185-
}
186-
}
187176
#endif

Tests/MutexTests.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 Timeout
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.storage.lock()
59+
#expect(
60+
mutex.withLockIfAvailable { _ in "chips" } == nil
61+
)
62+
mutex.storage.unlock()
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

Tests/MutexXCTests.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 Timeout
34+
import XCTest
35+
36+
final class MutexXCTests: 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.storage.lock()
56+
XCTAssertNil(
57+
mutex.withLockIfAvailable { _ in "chips" }
58+
)
59+
mutex.storage.unlock()
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

0 commit comments

Comments
 (0)