Skip to content

Commit e3dcfeb

Browse files
authored
feat(TaskOperation): allow executing as detached task (#7)
1 parent 63fed91 commit e3dcfeb

File tree

7 files changed

+171
-89
lines changed

7 files changed

+171
-89
lines changed

AsyncObjects.xcodeproj/project.pbxproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
/* End PBXAggregateTarget section */
2222

2323
/* Begin PBXBuildFile section */
24-
64DDE914E91EBCF451EEB28E /* AsyncObjects.docc in Sources */ = {isa = PBXBuildFile; fileRef = 2F994B835A88B84F6C8AE38B /* AsyncObjects.docc */; };
24+
15423F4ABF75D8A400B6C633 /* AsyncObjects.docc in Sources */ = {isa = PBXBuildFile; fileRef = 92104D54C38BC7BB7060DA49 /* AsyncObjects.docc */; };
2525
OBJ_106 /* AsyncCountdownEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* AsyncCountdownEvent.swift */; };
2626
OBJ_107 /* AsyncEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* AsyncEvent.swift */; };
2727
OBJ_108 /* AsyncObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* AsyncObject.swift */; };
@@ -30,7 +30,7 @@
3030
OBJ_111 /* Continuable.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Continuable.swift */; };
3131
OBJ_112 /* Future.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Future.swift */; };
3232
OBJ_113 /* Locker.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* Locker.swift */; };
33-
OBJ_114 /* TaskGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* TaskGroup.swift */; };
33+
OBJ_114 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* Task.swift */; };
3434
OBJ_115 /* TaskOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* TaskOperation.swift */; };
3535
OBJ_116 /* TaskQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* TaskQueue.swift */; };
3636
OBJ_117 /* TaskTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* TaskTracker.swift */; };
@@ -104,7 +104,7 @@
104104
/* End PBXBuildFile section */
105105

106106
/* Begin PBXFileReference section */
107-
2F994B835A88B84F6C8AE38B /* AsyncObjects.docc */ = {isa = PBXFileReference; includeInIndex = 1; path = AsyncObjects.docc; sourceTree = "<group>"; };
107+
92104D54C38BC7BB7060DA49 /* AsyncObjects.docc */ = {isa = PBXFileReference; includeInIndex = 1; path = AsyncObjects.docc; sourceTree = "<group>"; };
108108
OBJ_11 /* AsyncCountdownEvent.swift */ = {isa = PBXFileReference; path = AsyncCountdownEvent.swift; sourceTree = "<group>"; };
109109
OBJ_12 /* AsyncEvent.swift */ = {isa = PBXFileReference; path = AsyncEvent.swift; sourceTree = "<group>"; };
110110
OBJ_13 /* AsyncObject.swift */ = {isa = PBXFileReference; path = AsyncObject.swift; sourceTree = "<group>"; };
@@ -113,7 +113,7 @@
113113
OBJ_16 /* Continuable.swift */ = {isa = PBXFileReference; path = Continuable.swift; sourceTree = "<group>"; };
114114
OBJ_17 /* Future.swift */ = {isa = PBXFileReference; path = Future.swift; sourceTree = "<group>"; };
115115
OBJ_18 /* Locker.swift */ = {isa = PBXFileReference; path = Locker.swift; sourceTree = "<group>"; };
116-
OBJ_19 /* TaskGroup.swift */ = {isa = PBXFileReference; path = TaskGroup.swift; sourceTree = "<group>"; };
116+
OBJ_19 /* Task.swift */ = {isa = PBXFileReference; path = Task.swift; sourceTree = "<group>"; };
117117
OBJ_20 /* TaskOperation.swift */ = {isa = PBXFileReference; path = TaskOperation.swift; sourceTree = "<group>"; };
118118
OBJ_21 /* TaskQueue.swift */ = {isa = PBXFileReference; path = TaskQueue.swift; sourceTree = "<group>"; };
119119
OBJ_22 /* TaskTracker.swift */ = {isa = PBXFileReference; path = TaskTracker.swift; sourceTree = "<group>"; };
@@ -220,11 +220,11 @@
220220
OBJ_16 /* Continuable.swift */,
221221
OBJ_17 /* Future.swift */,
222222
OBJ_18 /* Locker.swift */,
223-
OBJ_19 /* TaskGroup.swift */,
223+
OBJ_19 /* Task.swift */,
224224
OBJ_20 /* TaskOperation.swift */,
225225
OBJ_21 /* TaskQueue.swift */,
226226
OBJ_22 /* TaskTracker.swift */,
227-
2F994B835A88B84F6C8AE38B /* AsyncObjects.docc */,
227+
92104D54C38BC7BB7060DA49 /* AsyncObjects.docc */,
228228
);
229229
name = AsyncObjects;
230230
path = Sources/AsyncObjects;
@@ -556,11 +556,11 @@
556556
OBJ_111 /* Continuable.swift in Sources */,
557557
OBJ_112 /* Future.swift in Sources */,
558558
OBJ_113 /* Locker.swift in Sources */,
559-
OBJ_114 /* TaskGroup.swift in Sources */,
559+
OBJ_114 /* Task.swift in Sources */,
560560
OBJ_115 /* TaskOperation.swift in Sources */,
561561
OBJ_116 /* TaskQueue.swift in Sources */,
562562
OBJ_117 /* TaskTracker.swift in Sources */,
563-
64DDE914E91EBCF451EEB28E /* AsyncObjects.docc in Sources */,
563+
15423F4ABF75D8A400B6C633 /* AsyncObjects.docc in Sources */,
564564
);
565565
};
566566
OBJ_126 /* Sources */ = {

Sources/AsyncObjects/AsyncCountdownEvent.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ public actor AsyncCountdownEvent: AsyncObject {
4747

4848
// MARK: Internal
4949

50+
/// Resume provided continuation with additional changes based on the associated flags.
51+
///
52+
/// - Parameter continuation: The queued continuation to resume.
53+
@inlinable
54+
func _resumeContinuation(_ continuation: Continuation) {
55+
currentCount += 1
56+
continuation.resume()
57+
}
58+
5059
/// Add continuation with the provided key in `continuations` map.
5160
///
5261
/// - Parameters:
@@ -58,8 +67,7 @@ public actor AsyncCountdownEvent: AsyncObject {
5867
withKey key: UUID
5968
) {
6069
guard !isSet, continuations.isEmpty else {
61-
currentCount += 1
62-
continuation.resume()
70+
_resumeContinuation(continuation)
6371
return
6472
}
6573
continuations[key] = continuation
@@ -90,8 +98,7 @@ public actor AsyncCountdownEvent: AsyncObject {
9098
func _resumeContinuations() {
9199
while !continuations.isEmpty && isSet {
92100
let (_, continuation) = continuations.removeFirst()
93-
continuation.resume()
94-
self.currentCount += 1
101+
_resumeContinuation(continuation)
95102
}
96103
}
97104

Sources/AsyncObjects/TaskOperation.swift

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import Dispatch
1818
public final class TaskOperation<R: Sendable>: Operation, AsyncObject,
1919
@unchecked Sendable
2020
{
21-
/// The type used to track completion of provided operation and unstructured tasks created in it.
22-
private typealias Tracker = TaskTracker
2321
/// The asynchronous action to perform as part of the operation..
2422
private let underlyingAction: @Sendable () async throws -> R
2523
/// The top-level task that executes asynchronous action provided
@@ -29,18 +27,23 @@ public final class TaskOperation<R: Sendable>: Operation, AsyncObject,
2927
/// synchronize data access and modifications.
3028
@usableFromInline
3129
let locker: Locker
30+
31+
/// A type representing a set of behaviors for the executed
32+
/// task type and task completion behavior.
33+
///
34+
/// ``TaskOperation`` determines the execution behavior of
35+
/// provided action as task based on the provided flags.
36+
public typealias Flags = TaskOperationFlags
3237
/// The priority of top-level task executed.
3338
///
3439
/// In case of `nil` priority from `Task.currentPriority`
3540
/// of task that starts the operation used.
3641
public let priority: TaskPriority?
37-
/// If completion of unstructured tasks created as part of provided task
38-
/// should be tracked.
42+
/// A set of behaviors for the executed task type and task completion behavior.
3943
///
40-
/// If true, operation only completes if the provided asynchronous action
41-
/// and all of its created unstructured task completes.
42-
/// Otherwise, operation completes if the provided action itself completes.
43-
public let shouldTrackUnstructuredTasks: Bool
44+
/// Provided flags determine the execution behavior of
45+
/// the action as task.
46+
public let flags: Flags
4447

4548
/// A Boolean value indicating whether the operation executes its task asynchronously.
4649
///
@@ -119,14 +122,14 @@ public final class TaskOperation<R: Sendable>: Operation, AsyncObject,
119122
///
120123
/// - Returns: The newly created asynchronous operation.
121124
public init(
122-
trackUnstructuredTasks shouldTrackUnstructuredTasks: Bool = false,
123125
synchronizedWith locker: Locker = .init(),
124126
priority: TaskPriority? = nil,
127+
flags: Flags = [],
125128
operation: @escaping @Sendable () async throws -> R
126129
) {
127-
self.shouldTrackUnstructuredTasks = shouldTrackUnstructuredTasks
128130
self.locker = locker
129131
self.priority = priority
132+
self.flags = flags
130133
self.underlyingAction = operation
131134
super.init()
132135
}
@@ -154,22 +157,12 @@ public final class TaskOperation<R: Sendable>: Operation, AsyncObject,
154157
/// as part of a new top-level task on behalf of the current actor.
155158
public override func main() {
156159
guard isExecuting, execTask == nil else { return }
157-
execTask = Task(priority: priority) { [weak self] in
158-
guard
159-
let action = self?.underlyingAction,
160-
let trackUnstructuredTasks = self?.shouldTrackUnstructuredTasks
161-
else { throw CancellationError() }
162-
let final = { @Sendable[weak self] in self?._finish(); return }
163-
return trackUnstructuredTasks
164-
? try await Tracker.$current.withValue(
165-
.init(onComplete: final),
166-
operation: action
167-
)
168-
: try await {
169-
defer { final() }
170-
return try await action()
171-
}()
172-
}
160+
let final = { @Sendable[weak self] in self?._finish(); return }
161+
execTask = flags.createTask(
162+
priority: priority,
163+
operation: underlyingAction,
164+
onComplete: final
165+
)
173166
}
174167

175168
/// Advises the operation object that it should stop executing its task.
@@ -275,3 +268,86 @@ public final class TaskOperation<R: Sendable>: Operation, AsyncObject,
275268
/// if the operation hasn't been started yet with either
276269
/// ``TaskOperation/start()`` or ``TaskOperation/signal()``.
277270
public struct EarlyInvokeError: Error, Sendable {}
271+
272+
/// A set of behaviors for ``TaskOperation``s,
273+
/// such as the task type and task completion behavior.
274+
///
275+
/// ``TaskOperation`` determines the execution behavior of
276+
/// provided action as task based on the provided flags.
277+
public struct TaskOperationFlags: OptionSet, Sendable {
278+
/// Indicates to ``TaskOperation``, completion of unstructured tasks
279+
/// created as part of provided operation should be tracked.
280+
///
281+
/// If provided, GCD operation only completes if the provided asynchronous action
282+
/// and all of its created unstructured task completes.
283+
/// Otherwise, operation completes if the provided action itself completes.
284+
public static let trackUnstructuredTasks = Self.init(rawValue: 1 << 0)
285+
/// Indicates to ``TaskOperation`` to disassociate action from the current execution context
286+
/// by running as a new detached task.
287+
///
288+
/// Provided action is executed asynchronously as part of a new top-level task,
289+
/// with the provided task priority and without inheriting actor context that started
290+
/// the GCD operation.
291+
public static let detached = Self.init(rawValue: 1 << 1)
292+
293+
/// The type used to track completion of provided operation and unstructured tasks created in it.
294+
private typealias Tracker = TaskTracker
295+
296+
/// Runs the given throwing operation asynchronously as part of a new top-level task
297+
/// based on the current flags indicating whether to on behalf of the current actor
298+
/// and whether to track unstructured tasks created in provided operation.
299+
///
300+
/// - Parameters:
301+
/// - priority: The priority of the task that operation executes.
302+
/// Pass `nil` to use the priority from `Task.currentPriority`
303+
/// of task that starts the operation.
304+
/// - operation: The asynchronous operation to execute.
305+
/// - completion: The action to invoke when task completes.
306+
///
307+
/// - Returns: A reference to the task.
308+
fileprivate func createTask<R: Sendable>(
309+
priority: TaskPriority? = nil,
310+
operation: @escaping @Sendable () async throws -> R,
311+
onComplete completion: @escaping @Sendable () -> Void
312+
) -> Task<R, Error> {
313+
typealias LocalTask = Task<R, Error>
314+
typealias ThrowingAction = @Sendable () async throws -> R
315+
typealias TaskInitializer = (TaskPriority?, ThrowingAction) -> LocalTask
316+
317+
let initializer =
318+
self.contains(.detached)
319+
? LocalTask.detached
320+
: LocalTask.init
321+
return initializer(priority) {
322+
return self.contains(.trackUnstructuredTasks)
323+
? try await Tracker.$current.withValue(
324+
.init(onComplete: completion),
325+
operation: operation
326+
)
327+
: try await {
328+
defer { completion() }
329+
return try await operation()
330+
}()
331+
}
332+
}
333+
334+
/// The corresponding value of the raw type.
335+
///
336+
/// A new instance initialized with rawValue will be equivalent to this instance.
337+
/// For example:
338+
/// ```swift
339+
/// print(TaskOperationFlags(rawValue: 1 << 1) == TaskOperationFlags.detached)
340+
/// // Prints "true"
341+
/// ```
342+
public let rawValue: UInt8
343+
/// Creates a new flag from the given raw value.
344+
///
345+
/// - Parameter rawValue: The raw value of the flag set to create.
346+
/// - Returns: The newly created flag set.
347+
///
348+
/// - Note: Do not use this method to create flag,
349+
/// use the default flags provided instead.
350+
public init(rawValue: UInt8) {
351+
self.rawValue = rawValue
352+
}
353+
}

Sources/AsyncObjects/TaskQueue.swift

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -162,24 +162,37 @@ public actor TaskQueue: AsyncObject {
162162
|| flags.wait(forCurrent: currentRunning)
163163
}
164164

165+
/// Resume provided continuation with additional changes based on the associated flags.
166+
///
167+
/// - Parameter continuation: The queued continuation to resume.
168+
/// - Returns: Whether queue is free to proceed scheduling other tasks.
169+
@inlinable
170+
@discardableResult
171+
func _resumeQueuedContinuation(
172+
_ continuation: QueuedContinuation
173+
) -> Bool {
174+
currentRunning += 1
175+
continuation.value.resume()
176+
guard continuation.flags.isBlockEnabled else { return true }
177+
blocked = true
178+
return false
179+
}
180+
165181
/// Add continuation with the provided key and associated flags to queue.
166182
///
167183
/// - Parameters:
168-
/// - flags: The flags associated with continuation operation.
169184
/// - key: The key in the continuation queue.
170-
/// - continuation: The continuation to add to queue.
185+
/// - continuation: The continuation and flags to add to queue.
171186
@inlinable
172187
func _queueContinuation(
173-
withFlags flags: Flags = [],
174188
atKey key: UUID = .init(),
175-
_ continuation: Continuation
189+
_ continuation: QueuedContinuation
176190
) {
177-
guard _wait(whenFlags: flags) else {
178-
currentRunning += 1
179-
continuation.resume()
191+
guard _wait(whenFlags: continuation.flags) else {
192+
_resumeQueuedContinuation(continuation)
180193
return
181194
}
182-
queue[key] = (value: continuation, flags: flags)
195+
queue[key] = continuation
183196
}
184197

185198
/// Remove continuation associated with provided key from queue.
@@ -218,16 +231,12 @@ public actor TaskQueue: AsyncObject {
218231
/// and operation flags preconditions satisfied.
219232
@inlinable
220233
func _resumeQueuedTasks() {
221-
while let (_, (continuation, flags)) = queue.elements.first,
234+
while let (_, continuation) = queue.elements.first,
222235
!blocked,
223-
!flags.wait(forCurrent: currentRunning)
236+
!continuation.flags.wait(forCurrent: currentRunning)
224237
{
225238
queue.removeFirst()
226-
currentRunning += 1
227-
continuation.resume()
228-
guard flags.isBlockEnabled else { continue }
229-
blocked = true
230-
break
239+
guard _resumeQueuedContinuation(continuation) else { break }
231240
}
232241
}
233242

@@ -252,9 +261,8 @@ public actor TaskQueue: AsyncObject {
252261
try await Continuation.with { continuation in
253262
Task { [weak self] in
254263
await self?._queueContinuation(
255-
withFlags: flags,
256264
atKey: key,
257-
continuation
265+
(value: continuation, flags: flags)
258266
)
259267
}
260268
}

Tests/AsyncObjectsTests/StandardLibraryTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import XCTest
22

33
/// Tests inner workings of structured concurrency
4-
@MainActor
54
class StandardLibraryTests: XCTestCase {
65

76
func testTaskValueFetchingCancelation() async throws {

Tests/AsyncObjectsTests/TaskOperationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ class TaskOperationTests: XCTestCase {
184184
func createOperationWithChildTasks(
185185
track: Bool = false
186186
) -> TaskOperation<Void> {
187-
return TaskOperation(trackUnstructuredTasks: track) {
187+
return TaskOperation(flags: track ? .trackUnstructuredTasks : []) {
188188
Task {
189189
try await Self.sleep(seconds: 1)
190190
}

0 commit comments

Comments
 (0)