Skip to content

Commit 2ecbb15

Browse files
committed
feat: Implement DispatchGroup using Swift Concurrency.
1 parent d90e8b8 commit 2ecbb15

File tree

1 file changed

+129
-0
lines changed

1 file changed

+129
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
// MARK: - Public Interface for Non-Async Usage -
14+
15+
/// `DispatchGroup` is a drop-in replacement for the `DispatchGroup` implemented
16+
/// in Grand Central Dispatch. However, this class uses Swift Concurrency, instead of low-level threading API's.
17+
///
18+
/// The primary goal of this implementation is to enable WASM support for Dispatch.
19+
///
20+
/// Refer to documentation for the original [DispatchGroup](https://developer.apple.com/documentation/dispatch/dispatchgroup)
21+
/// for more details,
22+
@available(macOS 10.15, *)
23+
public class DispatchGroup: @unchecked Sendable {
24+
/// Used to ensure FIFO access to the enter and leave calls
25+
@globalActor
26+
private actor DispatchGroupEntryActor: GlobalActor {
27+
static let shared = DispatchGroupEntryActor()
28+
}
29+
30+
private let group = AsyncGroup()
31+
32+
public func enter() {
33+
Task { @DispatchGroupEntryActor [] in
34+
// ^--- Ensures serial FIFO entrance/exit into the group
35+
await group.enter()
36+
}
37+
}
38+
39+
public func leave() {
40+
Task { @DispatchGroupEntryActor [] in
41+
// ^--- Ensures serial FIFO entrance/exit into the group
42+
await group.leave()
43+
}
44+
}
45+
46+
public func notify(queue: DispatchQueue, execute work: @escaping @Sendable @convention(block) () -> Void) {
47+
Task { @DispatchGroupEntryActor [] in
48+
// ^--- Ensures serial FIFO entrance/exit into the group
49+
await group.notify {
50+
await withCheckedContinuation { continuation in
51+
queue.async {
52+
work()
53+
continuation.resume()
54+
}
55+
}
56+
}
57+
}
58+
}
59+
60+
func wait() async {
61+
await group.wait()
62+
}
63+
64+
public init() {}
65+
}
66+
67+
// MARK: - Private Interface for Async Usage -
68+
69+
@available(macOS 10.15, *)
70+
fileprivate actor AsyncGroup {
71+
private var taskCount = 0
72+
private var continuation: CheckedContinuation<Void, Never>?
73+
private var isWaiting = false
74+
private var notifyHandlers: [@Sendable () async -> Void] = []
75+
76+
func enter() {
77+
taskCount += 1
78+
}
79+
80+
func leave() {
81+
defer {
82+
checkCompletion()
83+
}
84+
guard taskCount > 0 else {
85+
assertionFailure("leave() called more times than enter()")
86+
return
87+
}
88+
taskCount -= 1
89+
}
90+
91+
func notify(handler: @escaping @Sendable () async -> Void) {
92+
notifyHandlers.append(handler)
93+
checkCompletion()
94+
}
95+
96+
func wait() async {
97+
if taskCount <= 0 {
98+
return
99+
}
100+
101+
isWaiting = true
102+
103+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
104+
self.continuation = continuation
105+
checkCompletion()
106+
}
107+
}
108+
109+
private func checkCompletion() {
110+
if taskCount <= 0 {
111+
if isWaiting {
112+
continuation?.resume()
113+
continuation = nil
114+
isWaiting = false
115+
}
116+
117+
if !notifyHandlers.isEmpty {
118+
let handlers = notifyHandlers
119+
notifyHandlers.removeAll()
120+
121+
for handler in handlers {
122+
Task {
123+
await handler()
124+
}
125+
}
126+
}
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)