@@ -34,7 +34,7 @@ fileprivate extension NSLock {
34
34
}
35
35
36
36
/// A type that is able to track dependencies between tasks.
37
- public protocol DependencyTracker {
37
+ public protocol DependencyTracker : Sendable {
38
38
/// Whether the task described by `self` needs to finish executing before
39
39
/// `other` can start executing.
40
40
func isDependency( of other: Self ) -> Bool
@@ -48,29 +48,47 @@ public struct Serial: DependencyTracker {
48
48
}
49
49
}
50
50
51
- /// A queue that allows the execution of asynchronous blocks of code.
52
- public final class AsyncQueue < TaskMetadata: DependencyTracker > {
53
- private struct PendingTask {
54
- /// The task that is pending.
55
- let task : any AnyTask
51
+ private struct PendingTask < TaskMetadata: Sendable > : Sendable {
52
+ /// The task that is pending.
53
+ let task : any AnyTask
56
54
57
- let metadata : TaskMetadata
55
+ let metadata : TaskMetadata
58
56
59
- /// A unique value used to identify the task. This allows tasks to get
60
- /// removed from `pendingTasks` again after they finished executing.
61
- let id : UUID
62
- }
57
+ /// A unique value used to identify the task. This allows tasks to get
58
+ /// removed from `pendingTasks` again after they finished executing.
59
+ let id : UUID
60
+ }
63
61
62
+ /// A list of pending tasks that can be sent across actor boundaries and is guarded by a lock.
63
+ ///
64
+ /// - Note: Unchecked sendable because the tasks are being protected by a lock.
65
+ private class PendingTasks < TaskMetadata: Sendable > : @unchecked Sendable {
64
66
/// Lock guarding `pendingTasks`.
65
- private let pendingTasksLock = NSLock ( )
67
+ private let lock = NSLock ( )
66
68
67
69
/// Pending tasks that have not finished execution yet.
68
- private var pendingTasks = [ PendingTask] ( )
70
+ ///
71
+ /// - Important: This must only be accessed while `lock` has been acquired.
72
+ private var tasks : [ PendingTask < TaskMetadata > ] = [ ]
69
73
70
- public init ( ) {
71
- self . pendingTasksLock . name = " AsyncQueue "
74
+ init ( ) {
75
+ self . lock . name = " AsyncQueue "
72
76
}
73
77
78
+ /// Capture a lock and execute the closure, which may modify the pending tasks.
79
+ func withLock< T> ( _ body: ( _ pendingTasks: inout [ PendingTask < TaskMetadata > ] ) throws -> T ) rethrows -> T {
80
+ try lock. withLock {
81
+ try body ( & tasks)
82
+ }
83
+ }
84
+ }
85
+
86
+ /// A queue that allows the execution of asynchronous blocks of code.
87
+ public final class AsyncQueue < TaskMetadata: DependencyTracker > {
88
+ private var pendingTasks : PendingTasks < TaskMetadata > = PendingTasks ( )
89
+
90
+ public init ( ) { }
91
+
74
92
/// Schedule a new closure to be executed on the queue.
75
93
///
76
94
/// If this is a serial queue, all previously added tasks are guaranteed to
@@ -108,13 +126,13 @@ public final class AsyncQueue<TaskMetadata: DependencyTracker> {
108
126
) -> Task < Success , any Error > {
109
127
let id = UUID ( )
110
128
111
- return pendingTasksLock . withLock {
129
+ return pendingTasks . withLock { tasks in
112
130
// Build the list of tasks that need to finished execution before this one
113
131
// can be executed
114
- let dependencies : [ PendingTask ] = pendingTasks . filter { $0. metadata. isDependency ( of: metadata) }
132
+ let dependencies : [ PendingTask ] = tasks . filter { $0. metadata. isDependency ( of: metadata) }
115
133
116
134
// Schedule the task.
117
- let task = Task {
135
+ let task = Task { [ pendingTasks ] in
118
136
// IMPORTANT: The only throwing call in here must be the call to
119
137
// operation. Otherwise the assumption that the task will never throw
120
138
// if `operation` does not throw, which we are making in `async` does
@@ -125,14 +143,14 @@ public final class AsyncQueue<TaskMetadata: DependencyTracker> {
125
143
126
144
let result = try await operation ( )
127
145
128
- pendingTasksLock . withLock {
129
- pendingTasks . removeAll ( where: { $0. id == id } )
146
+ pendingTasks . withLock { tasks in
147
+ tasks . removeAll ( where: { $0. id == id } )
130
148
}
131
149
132
150
return result
133
151
}
134
152
135
- pendingTasks . append ( PendingTask ( task: task, metadata: metadata, id: id) )
153
+ tasks . append ( PendingTask ( task: task, metadata: metadata, id: id) )
136
154
137
155
return task
138
156
}
0 commit comments