Skip to content

Commit 6ab8b55

Browse files
committed
Cleanup
1 parent c8e0ee5 commit 6ab8b55

File tree

3 files changed

+41
-24
lines changed

3 files changed

+41
-24
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Task {
1919
semaphore.signal()
2020
```
2121

22-
The `wait()` method has a `waitUntilTaskCancellation()` variant that throws `CancellationError` if the task is canceled before a signal occurs.
22+
The `wait()` method has a `waitUnlessCancelled()` variant that throws `CancellationError` if the task is cancelled before a signal occurs.
2323

2424
For a nice introduction to semaphores, see [The Beauty of Semaphores in Swift 🚦](https://medium.com/@roykronenfeld/semaphores-in-swift-e296ea80f860). The article discusses [`DispatchSemaphore`], but it can easily be ported to Swift concurrency: see the [demo playground](Demo/SemaphorePlayground.playground/Contents.swift) of this package.
2525

Sources/Semaphore/Semaphore.swift

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,24 @@ import Foundation
3737
///
3838
/// - ``signal()``
3939
///
40-
/// ### Blocking on the Semaphore
40+
/// ### Waiting for the Semaphore
4141
///
4242
/// - ``wait()``
43-
/// - ``waitUntilTaskCancellation()``
43+
/// - ``waitUnlessCancelled()``
4444
public final class Semaphore {
45-
/// The semaphore value.
46-
private var value: Int
47-
45+
/// "Waiting for a signal" is easily said, but several possible states exist.
4846
private class Suspension {
4947
enum State {
48+
/// Initial state. Next is suspended, or cancelled.
5049
case pending
51-
case suspendedUntilTaskCancellation(UnsafeContinuation<Void, Error>)
50+
51+
/// Waiting for a signal, with support for cancellation.
52+
case suspendedUnlessCancelled(UnsafeContinuation<Void, Error>)
53+
54+
/// Waiting for a signal, with no support for cancellation.
5255
case suspended(UnsafeContinuation<Void, Never>)
56+
57+
/// Cancelled before we have started waiting.
5358
case cancelled
5459
}
5560

@@ -64,15 +69,25 @@ public final class Semaphore {
6469
}
6570
}
6671

72+
// MARK: - Internal State
73+
74+
/// The semaphore value.
75+
private var value: Int
76+
77+
/// As many elements as there are suspended tasks waiting for a signal.
78+
/// We store `Suspension` instances instead of `UnsafeContinuation`, because
79+
/// we support cancellation by removing `Suspension` instances from
80+
/// this array.
6781
private var suspensions: [Suspension] = []
6882

69-
/// This lock would be required even if ``Semaphore`` were made an actor,
70-
/// because `withUnsafeContinuation` suspends before it runs its closure
71-
/// argument. Also, by making ``Semaphore`` a plain class, we can expose a
72-
/// non-async ``signal()`` method. The lock is recursive in order to handle
73-
/// cancellation (see the implementation of ``wait()``).
83+
/// The lock that protects `value` and `suspensions`.
84+
///
85+
/// It is recursive in order to handle cancellation (see the implementation
86+
/// of ``waitUnlessCancelled()``).
7487
private let lock = NSRecursiveLock()
7588

89+
// MARK: - Creating a Semaphore
90+
7691
/// Creates a semaphore.
7792
///
7893
/// - parameter value: The starting value for the semaphore. Do not pass a
@@ -86,6 +101,8 @@ public final class Semaphore {
86101
precondition(suspensions.isEmpty, "Semaphore is deallocated while some task(s) are suspended waiting for a signal.")
87102
}
88103

104+
// MARK: - Waiting for the Semaphore
105+
89106
/// Waits for, or decrements, a semaphore.
90107
///
91108
/// Decrement the counting semaphore. If the resulting value is less than
@@ -119,7 +136,7 @@ public final class Semaphore {
119136
///
120137
/// - Throws: If the task is canceled before a signal occurs, this function
121138
/// throws `CancellationError`.
122-
public func waitUntilTaskCancellation() async throws {
139+
public func waitUnlessCancelled() async throws {
123140
lock.lock()
124141

125142
value -= 1
@@ -148,7 +165,7 @@ public final class Semaphore {
148165
// The first suspended task will be the first task resumed by `signal`.
149166
// This is not intended to be a strong fifo guarantee, but just
150167
// an attempt at some fairness.
151-
suspension.state = .suspendedUntilTaskCancellation(continuation)
168+
suspension.state = .suspendedUnlessCancelled(continuation)
152169
suspensions.insert(suspension, at: 0)
153170
lock.unlock()
154171
}
@@ -168,7 +185,7 @@ public final class Semaphore {
168185
suspensions.remove(at: index)
169186
}
170187

171-
if case let .suspendedUntilTaskCancellation(continuation) = suspension.state {
188+
if case let .suspendedUnlessCancelled(continuation) = suspension.state {
172189
// Task is cancelled while suspended: resume with a CancellationError.
173190
continuation.resume(throwing: CancellationError())
174191
} else {
@@ -179,6 +196,8 @@ public final class Semaphore {
179196
}
180197
}
181198

199+
// MARK: - Signaling the Semaphore
200+
182201
/// Signals (increments) a semaphore.
183202
///
184203
/// Increment the counting semaphore. If the previous value was less than
@@ -194,16 +213,14 @@ public final class Semaphore {
194213
value += 1
195214

196215
switch suspensions.popLast()?.state {
197-
case let .suspendedUntilTaskCancellation(continuation):
216+
case let .suspendedUnlessCancelled(continuation):
198217
continuation.resume()
199218
return true
200219
case let .suspended(continuation):
201220
continuation.resume()
202221
return true
203222
default:
204-
break
223+
return false
205224
}
206-
207-
return false
208225
}
209226
}

Tests/SemaphoreTests/SemaphoreTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ final class SemaphoreTests: XCTestCase {
124124
let ex = expectation(description: "cancellation")
125125
let task = Task {
126126
do {
127-
try await sem.waitUntilTaskCancellation()
127+
try await sem.waitUnlessCancelled()
128128
XCTFail("Expected CancellationError")
129129
} catch is CancellationError {
130130
} catch {
@@ -148,7 +148,7 @@ final class SemaphoreTests: XCTestCase {
148148
}
149149
}
150150
do {
151-
try await sem.waitUntilTaskCancellation()
151+
try await sem.waitUnlessCancelled()
152152
XCTFail("Expected CancellationError")
153153
} catch is CancellationError {
154154
} catch {
@@ -164,7 +164,7 @@ final class SemaphoreTests: XCTestCase {
164164
// Given a task cancelled while suspended on a semaphore,
165165
let sem = Semaphore(value: 0)
166166
let task = Task {
167-
try await sem.waitUntilTaskCancellation()
167+
try await sem.waitUnlessCancelled()
168168
}
169169
try await Task.sleep(nanoseconds: 100_000_000)
170170
task.cancel()
@@ -197,7 +197,7 @@ final class SemaphoreTests: XCTestCase {
197197
continuation.resume()
198198
}
199199
}
200-
try await sem.waitUntilTaskCancellation()
200+
try await sem.waitUnlessCancelled()
201201
}
202202
task.cancel()
203203

@@ -279,7 +279,7 @@ final class SemaphoreTests: XCTestCase {
279279
await withThrowingTaskGroup(of: Void.self) { group in
280280
for _ in 0..<(maxCount * 2) {
281281
group.addTask {
282-
try await sem.waitUntilTaskCancellation()
282+
try await sem.waitUnlessCancelled()
283283
await runner.run()
284284
sem.signal()
285285
}

0 commit comments

Comments
 (0)