Skip to content

Commit 8731258

Browse files
committed
[Concurrency] Introduce initial (minimal, incomplete) Nursery APIs
1 parent 5b8e514 commit 8731258

File tree

6 files changed

+310
-2
lines changed

6 files changed

+310
-2
lines changed

stdlib/public/Concurrency/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_swift_target_library(swift_Concurrency ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} I
1919
_TimeTypes.swift
2020
TaskAlloc.cpp
2121
TaskStatus.cpp
22+
TaskNurseries.swift
2223
Mutex.cpp
2324

2425
SWIFT_MODULE_DEPENDS_OSX Darwin

stdlib/public/Concurrency/TaskCancellation.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ extension Task {
7878
/// if the current task has been cancelled.
7979
public struct CancellationError: Error {
8080
// no extra information, cancellation is intended to be light-weight
81+
public init() {}
8182
}
8283

8384
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
////===----------------------------------------------------------------------===//
2+
////
3+
//// This source file is part of the Swift.org open source project
4+
////
5+
//// Copyright (c) 2020 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+
import Swift
14+
@_implementationOnly import _SwiftConcurrencyShims
15+
16+
// ==== Task Nursery -----------------------------------------------------------
17+
18+
extension Task {
19+
20+
/// Starts a new nursery which provides a scope in which a dynamic number of
21+
/// tasks may be spawned.
22+
///
23+
/// Tasks added to the nursery by `nursery.add()` will automatically be
24+
/// awaited on when the scope exits.
25+
///
26+
/// ### Implicit awaiting
27+
/// When results of tasks added to the nursery need to be collected, one will
28+
/// gather task's results using the `while let result = await nursery.next() { ... }`
29+
/// pattern.
30+
///
31+
/// ### Cancellation
32+
/// If any of the tasks throws the nursery and all of its tasks will be cancelled,
33+
/// and the error will be re-thrown by `withNursery`.
34+
///
35+
/// Postcondition:
36+
/// Once `withNursery` returns it is guaranteed that the *nursery* is *empty*.
37+
///
38+
/// This is achieved in the following way:
39+
/// - if the body returns normally:
40+
/// - the nursery will await any not yet complete tasks,
41+
/// - if any of those tasks throws, the remaining tasks will be cancelled,
42+
/// - once the `withNursery` returns the nursery is guaranteed to be empty.
43+
/// - if the body throws:
44+
/// - all tasks remaining in the nursery will be automatically cancelled.
45+
///
46+
// TODO: Do we have to add a different nursery type to accommodate throwing
47+
// tasks without forcing users to use Result? I can't think of how that
48+
// could be propagated out of the callback body reasonably, unless we
49+
// commit to doing multi-statement closure typechecking.
50+
public static func withNursery<TaskResult, BodyResult>(
51+
resultType: TaskResult.Type,
52+
returning returnType: BodyResult.Type = BodyResult.self,
53+
body: (inout Nursery<TaskResult>) async throws -> BodyResult
54+
) async rethrows -> BodyResult {
55+
fatalError("\(#function) not implemented yet.")
56+
}
57+
58+
/// A nursery provides a scope within which a dynamic number of tasks may be
59+
/// started and added to the nursery.
60+
/* @unmoveable */
61+
public struct Nursery<TaskResult> {
62+
/// No public initializers
63+
private init() {}
64+
65+
// Swift will statically prevent this type from being copied or moved.
66+
// For now, that implies that it cannot be used with generics.
67+
68+
/// Add a child task to the nursery.
69+
///
70+
/// ### Error handling
71+
/// Operations are allowed to throw.
72+
///
73+
/// in which case the `await try next()`
74+
/// invocation corresponding to the failed task will re-throw the given task.
75+
///
76+
/// - Parameters:
77+
/// - overridingPriority: override priority of the operation task
78+
/// - operation: operation to execute and add to the nursery
79+
public mutating func add(
80+
overridingPriority: Priority? = nil,
81+
operation: () async throws -> TaskResult
82+
) async {
83+
fatalError("\(#function) not implemented yet.")
84+
}
85+
86+
/// Add a child task and return a `Task.Handle` that can be used to manage it.
87+
///
88+
/// The task's result is accessible either via the returned `handle` or the
89+
/// `nursery.next()` function (as any other `add`-ed task).
90+
///
91+
/// - Parameters:
92+
/// - overridingPriority: override priority of the operation task
93+
/// - operation: operation to execute and add to the nursery
94+
public mutating func addWithHandle(
95+
overridingPriority: Priority? = nil,
96+
operation: () async throws -> TaskResult
97+
) async -> Handle<TaskResult> {
98+
fatalError("\(#function) not implemented yet.")
99+
}
100+
101+
/// Wait for a child task to complete and return the result it returned,
102+
/// or else return.
103+
///
104+
///
105+
public mutating func next() async throws -> TaskResult? {
106+
fatalError("\(#function) not implemented yet.")
107+
}
108+
109+
/// Query whether the nursery has any remaining tasks.
110+
///
111+
/// Nurseries are always empty upon entry to the `withNursery` body, and
112+
/// become empty again when `withNursery` returns (either by awaiting on all
113+
/// pending tasks or cancelling them).
114+
///
115+
/// - Returns: `true` if the nursery has no pending tasks, `false` otherwise.
116+
public var isEmpty: Bool {
117+
fatalError("\(#function) not implemented yet.")
118+
}
119+
120+
/// Cancel all the remaining tasks in the nursery.
121+
///
122+
/// A cancelled nursery will not will NOT accept new tasks being added into it.
123+
///
124+
/// Any results, including errors thrown by tasks affected by this
125+
/// cancellation, are silently discarded.
126+
///
127+
/// - SeeAlso: `Task.addCancellationHandler`
128+
public mutating func cancelAll() {
129+
fatalError("\(#function) not implemented yet.")
130+
}
131+
}
132+
}

stdlib/public/Concurrency/TaskPrivate.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class AsyncTask;
2424
/// Initialize the task-local allocator in the given task.
2525
void _swift_task_alloc_initialize(AsyncTask *task);
2626

27-
/// Destsroy the task-local allocator in the given task.
27+
/// Destroy the task-local allocator in the given task.
2828
void _swift_task_alloc_destroy(AsyncTask *task);
2929

3030
} // end namespace swift

stdlib/public/Concurrency/_TimeTypes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ extension Task {
3838
}
3939

4040
public static func microseconds(_ us: UInt64) -> Self {
41-
.init(nanoseconds: clampedInt64Product(us, 1000))
41+
.init(nanoseconds: clampedInt64Product(us, 1_000))
4242
}
4343

4444
public static func nanoseconds(_ ns: UInt64) -> Self {
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// RUN: %target-typecheck-verify-swift -enable-experimental-concurrency
2+
// REQUIRES: concurrency
3+
4+
func asyncFunc() async -> Int { 42 }
5+
func asyncThrowsFunc() async throws -> Int { 42 }
6+
func asyncThrowsOnCancel() async throws -> Int {
7+
// terrible suspend-spin-loop -- do not do this
8+
// only for purposes of demonstration
9+
while await !Task.isCancelled() {
10+
await Task.sleep(until: Task.Deadline.in(.seconds(1)))
11+
}
12+
13+
throw Task.CancellationError()
14+
}
15+
16+
func test_nursery_add() async throws -> Int {
17+
await try Task.withNursery(resultType: Int.self) { nursery in
18+
await nursery.add {
19+
await asyncFunc()
20+
}
21+
22+
await nursery.add {
23+
await asyncFunc()
24+
}
25+
26+
var sum = 0
27+
while let v = await try nursery.next() {
28+
sum += v
29+
}
30+
return sum
31+
} // implicitly awaits
32+
}
33+
34+
func test_nursery_addHandles() async throws -> Int {
35+
await try Task.withNursery(resultType: Int.self) { nursery in
36+
let one = await nursery.addWithHandle {
37+
await asyncFunc()
38+
}
39+
40+
let two = await nursery.addWithHandle {
41+
await asyncFunc()
42+
}
43+
44+
_ = await try one.get()
45+
_ = await try two.get()
46+
} // implicitly awaits
47+
}
48+
49+
func test_nursery_cancel_handles() async throws {
50+
await try Task.withNursery(resultType: Int.self) { nursery in
51+
let one = await nursery.addWithHandle {
52+
await try asyncThrowsOnCancel()
53+
}
54+
55+
let two = await nursery.addWithHandle {
56+
await asyncFunc()
57+
}
58+
59+
_ = await try one.get()
60+
_ = await try two.get()
61+
} // implicitly awaits
62+
}
63+
64+
// ==== ------------------------------------------------------------------------
65+
// MARK: Example Nursery Usages
66+
67+
struct Boom: Error {}
68+
func work() async -> Int { 42 }
69+
func boom() async throws -> Int { throw Boom() }
70+
71+
func first_allMustSucceed() async throws {
72+
73+
let first: Int = await try Task.withNursery(resultType: Int.self) { nursery in
74+
await nursery.add { await work() }
75+
await nursery.add { await work() }
76+
await nursery.add { await try boom() }
77+
78+
if let first = await try nursery.next() {
79+
return first
80+
} else {
81+
fatalError("Should never happen, we either throw, or get a result from any of the tasks")
82+
}
83+
// implicitly await: boom
84+
}
85+
_ = first
86+
// Expected: re-thrown Boom
87+
}
88+
89+
func first_ignoreFailures() async throws {
90+
func work() async -> Int { 42 }
91+
func boom() async throws -> Int { throw Boom() }
92+
93+
let first: Int = await try Task.withNursery(resultType: Int.self) { nursery in
94+
await nursery.add { await work() }
95+
await nursery.add { await work() }
96+
await nursery.add {
97+
do {
98+
return await try boom()
99+
} catch {
100+
return 0 // TODO: until await try? works properly
101+
}
102+
}
103+
104+
var result: Int = 0
105+
while let v = await try nursery.next() {
106+
result = v
107+
108+
if result != 0 {
109+
break
110+
}
111+
}
112+
113+
return result
114+
}
115+
_ = first
116+
// Expected: re-thrown Boom
117+
}
118+
119+
// ==== ------------------------------------------------------------------------
120+
// MARK: Advanced Custom Nursery Usage
121+
122+
func test_nursery_quorum_thenCancel() async {
123+
// imitates a typical "gather quorum" routine that is typical in distributed systems programming
124+
enum Vote {
125+
case yay
126+
case nay
127+
}
128+
struct Follower {
129+
init(_ name: String) {}
130+
func vote() async throws -> Vote {
131+
// "randomly" vote yes or no
132+
return .yay
133+
}
134+
}
135+
136+
/// Performs a simple quorum vote among the followers.
137+
///
138+
/// - Returns: `true` iff `N/2 + 1` followers return `.yay`, `false` otherwise.
139+
func gatherQuorum(followers: [Follower]) async -> Bool {
140+
await try! Task.withNursery(resultType: Vote.self) { nursery in
141+
for follower in followers {
142+
await nursery.add { await try follower.vote() }
143+
}
144+
145+
defer {
146+
nursery.cancelAll()
147+
}
148+
149+
var yays: Int = 0
150+
var nays: Int = 0
151+
let quorum = Int(followers.count / 2) + 1
152+
while let vote = await try nursery.next() {
153+
switch vote {
154+
case .yay:
155+
yays += 1
156+
if yays >= quorum {
157+
// cancel all remaining voters, we already reached quorum
158+
return true
159+
}
160+
case .nay:
161+
nays += 1
162+
if nays >= quorum {
163+
return false
164+
}
165+
}
166+
}
167+
168+
return false
169+
}
170+
}
171+
172+
_ = await gatherQuorum(followers: [Follower("A"), Follower("B"), Follower("C")])
173+
}
174+

0 commit comments

Comments
 (0)