Skip to content

Commit 5108dec

Browse files
committed
feat: Implement DispatchSemaphore using Swift Concurrency.
1 parent 2ecbb15 commit 5108dec

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Provides a semaphore implantation in `async` context, with a safe wait method. Provides easy safer replacement
14+
/// for DispatchSemaphore usage.
15+
@available(macOS 10.15, *)
16+
actor AsyncSemaphore {
17+
private var value: Int
18+
private var waiters: Array<CheckedContinuation<Void, Never>> = []
19+
20+
init(value: Int = 1) {
21+
self.value = value
22+
}
23+
24+
func wait() async {
25+
value -= 1
26+
if value >= 0 { return }
27+
await withCheckedContinuation {
28+
waiters.append($0)
29+
}
30+
}
31+
32+
func signal() {
33+
self.value += 1
34+
35+
guard !waiters.isEmpty else { return }
36+
let first = waiters.removeFirst()
37+
first.resume()
38+
}
39+
}
40+
41+
@available(macOS 10.15, *)
42+
extension AsyncSemaphore {
43+
func withLock<T: Sendable>(_ closure: () async throws -> T) async rethrows -> T {
44+
await wait()
45+
defer { signal() }
46+
return try await closure()
47+
}
48+
49+
func withLockVoid(_ closure: () async throws -> Void) async rethrows {
50+
await wait()
51+
defer { signal() }
52+
try await closure()
53+
}
54+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
// This implementation assumes the single-threaded
14+
// environment that swift wasm executables typically run in.
15+
//
16+
// It is not appropriate for true multi-threaded environments.
17+
//
18+
// For safety, this class is only defined for WASI platforms.
19+
//
20+
//
21+
#if os(WASI)
22+
23+
/// DispatchSemaphore is not safe to use for most wasm executables.
24+
///
25+
/// Most wasm executables are single-threaded. Calling DispatchSemaphore.wait
26+
/// when it's value is 0 or lower would be likely cause a frozen main thread,
27+
/// because that would block the calling thread. And there is usually
28+
/// only one thread in the wasm world (right now).
29+
///
30+
/// For now, we guard against that case with both compile-time deprecation
31+
/// pointing to the much safer ``AsyncSemaphore``, and also at run-time with
32+
/// assertions.
33+
///
34+
/// ``AsyncSemaphore`` provides full functionality, but only exposes
35+
/// Swift Concurrency api's with a safe async wait function.
36+
@available(
37+
*,
38+
deprecated,
39+
renamed: "AsyncSemaphore",
40+
message: "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead."
41+
)
42+
@available(macOS 10.15, *)
43+
public class DispatchSemaphore: @unchecked Sendable {
44+
public var value: Int
45+
46+
public init(value: Int) {
47+
self.value = value
48+
}
49+
50+
@discardableResult
51+
public func signal() -> Int {
52+
MainActor.assertIsolated()
53+
value += 1
54+
return value
55+
}
56+
57+
public func wait() {
58+
// NOTE: wasm is currently mostly single threaded.
59+
// And we don't have a Thread.sleep API yet.
60+
// So
61+
MainActor.assertIsolated()
62+
assert(value > 0, "DispatchSemaphore is currently only designed for single-threaded use.")
63+
value -= 1
64+
}
65+
}
66+
67+
#else
68+
69+
@available(macOS 10.15, *)
70+
typealias DispatchSemaphore = AsyncSemaphore
71+
72+
#endif // #if os(WASI)

0 commit comments

Comments
 (0)