Skip to content

[Concurrency] Nurseries are now Task.Groups #34604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions include/swift/ABI/TaskStatus.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,8 @@ class DeadlineStatusRecord : public TaskStatusRecord {
/// A status record which states that a task has one or
/// more active child tasks.
class ChildTaskStatusRecord : public TaskStatusRecord {
/// FIXME: should this be an array? How are things like task
/// nurseries supposed to actually manage this? Should it be
/// atomically moodifiable?
/// FIXME: should this be an array? How are things like task groups supposed
/// to actually manage this? Should it be atomically modifiable?
AsyncTask *FirstChild;

public:
Expand Down
2 changes: 1 addition & 1 deletion stdlib/public/Concurrency/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ add_swift_target_library(swift_Concurrency ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} I
_TimeTypes.swift
TaskAlloc.cpp
TaskStatus.cpp
TaskNurseries.swift
TaskGroup.swift
Mutex.cpp

SWIFT_MODULE_DEPENDS_OSX Darwin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,59 +13,67 @@
import Swift
@_implementationOnly import _SwiftConcurrencyShims

// ==== Task Nursery -----------------------------------------------------------
// ==== Task Group -------------------------------------------------------------

extension Task {

/// Starts a new nursery which provides a scope in which a dynamic number of
/// Starts a new task group which provides a scope in which a dynamic number of
/// tasks may be spawned.
///
/// Tasks added to the nursery by `nursery.add()` will automatically be
/// awaited on when the scope exits.
/// Tasks added to the group by `group.add()` will automatically be awaited on
/// when the scope exits. If the group exits by throwing, all added tasks will
/// be cancelled and their results discarded.
///
/// ### Implicit awaiting
/// When results of tasks added to the nursery need to be collected, one will
/// gather task's results using the `while let result = await nursery.next() { ... }`
/// pattern.
/// When results of tasks added to the group need to be collected, one can
/// gather their results using the following pattern:
///
/// while let result = await group.next() {
/// // some accumulation logic (e.g. sum += result)
/// }
///
/// ### Cancellation
/// If any of the tasks throws the nursery and all of its tasks will be cancelled,
/// and the error will be re-thrown by `withNursery`.
/// If an error is thrown out of the task group, all of its remaining tasks
/// will be cancelled and the `withGroup` call will rethrow that error.
///
/// Individual tasks throwing results in their corresponding `try group.next()`
/// call throwing, giving a chance to handle individual errors or letting the
/// error be rethrown by the group.
///
/// Postcondition:
/// Once `withNursery` returns it is guaranteed that the *nursery* is *empty*.
/// Once `withGroup` returns it is guaranteed that the `group` is *empty*.
///
/// This is achieved in the following way:
/// - if the body returns normally:
/// - the nursery will await any not yet complete tasks,
/// - the group will await any not yet complete tasks,
/// - if any of those tasks throws, the remaining tasks will be cancelled,
/// - once the `withNursery` returns the nursery is guaranteed to be empty.
/// - once the `withGroup` returns the group is guaranteed to be empty.
/// - if the body throws:
/// - all tasks remaining in the nursery will be automatically cancelled.
///
// TODO: Do we have to add a different nursery type to accommodate throwing
/// - all tasks remaining in the group will be automatically cancelled.
// TODO: Do we have to add a different group type to accommodate throwing
// tasks without forcing users to use Result? I can't think of how that
// could be propagated out of the callback body reasonably, unless we
// commit to doing multi-statement closure typechecking.
public static func withNursery<TaskResult, BodyResult>(
public static func withGroup<TaskResult, BodyResult>(
resultType: TaskResult.Type,
returning returnType: BodyResult.Type = BodyResult.self,
body: (inout Nursery<TaskResult>) async throws -> BodyResult
body: (inout Task.Group<TaskResult>) async throws -> BodyResult
) async rethrows -> BodyResult {
fatalError("\(#function) not implemented yet.")
}

/// A nursery provides a scope within which a dynamic number of tasks may be
/// started and added to the nursery.
/// A task group serves as storage for dynamically started tasks.
///
/// Its intended use is with the
/* @unmoveable */
public struct Nursery<TaskResult> {
public struct Group<TaskResult> {
/// No public initializers
private init() {}

// Swift will statically prevent this type from being copied or moved.
// For now, that implies that it cannot be used with generics.

/// Add a child task to the nursery.
/// Add a child task to the group.
///
/// ### Error handling
/// Operations are allowed to throw.
Expand All @@ -75,7 +83,7 @@ extension Task {
///
/// - Parameters:
/// - overridingPriority: override priority of the operation task
/// - operation: operation to execute and add to the nursery
/// - operation: operation to execute and add to the group
public mutating func add(
overridingPriority: Priority? = nil,
operation: () async throws -> TaskResult
Expand All @@ -86,11 +94,11 @@ extension Task {
/// Add a child task and return a `Task.Handle` that can be used to manage it.
///
/// The task's result is accessible either via the returned `handle` or the
/// `nursery.next()` function (as any other `add`-ed task).
/// `group.next()` function (as any other `add`-ed task).
///
/// - Parameters:
/// - overridingPriority: override priority of the operation task
/// - operation: operation to execute and add to the nursery
/// - operation: operation to execute and add to the group
public mutating func addWithHandle(
overridingPriority: Priority? = nil,
operation: () async throws -> TaskResult
Expand All @@ -106,20 +114,20 @@ extension Task {
fatalError("\(#function) not implemented yet.")
}

/// Query whether the nursery has any remaining tasks.
/// Query whether the group has any remaining tasks.
///
/// Nurseries are always empty upon entry to the `withNursery` body, and
/// become empty again when `withNursery` returns (either by awaiting on all
/// Task groups are always empty upon entry to the `withGroup` body, and
/// become empty again when `withGroup` returns (either by awaiting on all
/// pending tasks or cancelling them).
///
/// - Returns: `true` if the nursery has no pending tasks, `false` otherwise.
/// - Returns: `true` if the group has no pending tasks, `false` otherwise.
public var isEmpty: Bool {
fatalError("\(#function) not implemented yet.")
}

/// Cancel all the remaining tasks in the nursery.
/// Cancel all the remaining tasks in the group.
///
/// A cancelled nursery will not will NOT accept new tasks being added into it.
/// A cancelled group will not will NOT accept new tasks being added into it.
///
/// Any results, including errors thrown by tasks affected by this
/// cancellation, are silently discarded.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,31 @@ func asyncThrowsOnCancel() async throws -> Int {
throw Task.CancellationError()
}

func test_nursery_add() async throws -> Int {
await try Task.withNursery(resultType: Int.self) { nursery in
await nursery.add {
func test_taskGroup_add() async throws -> Int {
await try Task.withGroup(resultType: Int.self) { group in
await group.add {
await asyncFunc()
}

await nursery.add {
await group.add {
await asyncFunc()
}

var sum = 0
while let v = await try nursery.next() {
while let v = await try group.next() {
sum += v
}
return sum
} // implicitly awaits
}

func test_nursery_addHandles() async throws -> Int {
await try Task.withNursery(resultType: Int.self) { nursery in
let one = await nursery.addWithHandle {
func test_taskGroup_addHandles() async throws -> Int {
await try Task.withGroup(resultType: Int.self) { group in
let one = await group.addWithHandle {
await asyncFunc()
}

let two = await nursery.addWithHandle {
let two = await group.addWithHandle {
await asyncFunc()
}

Expand All @@ -46,13 +46,13 @@ func test_nursery_addHandles() async throws -> Int {
} // implicitly awaits
}

func test_nursery_cancel_handles() async throws {
await try Task.withNursery(resultType: Int.self) { nursery in
let one = await nursery.addWithHandle {
func test_taskGroup_cancel_handles() async throws {
await try Task.withGroup(resultType: Int.self) { group in
let one = await group.addWithHandle {
await try asyncThrowsOnCancel()
}

let two = await nursery.addWithHandle {
let two = await group.addWithHandle {
await asyncFunc()
}

Expand All @@ -62,20 +62,20 @@ func test_nursery_cancel_handles() async throws {
}

// ==== ------------------------------------------------------------------------
// MARK: Example Nursery Usages
// MARK: Example group Usages

struct Boom: Error {}
func work() async -> Int { 42 }
func boom() async throws -> Int { throw Boom() }

func first_allMustSucceed() async throws {

let first: Int = await try Task.withNursery(resultType: Int.self) { nursery in
await nursery.add { await work() }
await nursery.add { await work() }
await nursery.add { await try boom() }
let first: Int = await try Task.withGroup(resultType: Int.self) { group in
await group.add { await work() }
await group.add { await work() }
await group.add { await try boom() }

if let first = await try nursery.next() {
if let first = await try group.next() {
return first
} else {
fatalError("Should never happen, we either throw, or get a result from any of the tasks")
Expand All @@ -90,10 +90,10 @@ func first_ignoreFailures() async throws {
func work() async -> Int { 42 }
func boom() async throws -> Int { throw Boom() }

let first: Int = await try Task.withNursery(resultType: Int.self) { nursery in
await nursery.add { await work() }
await nursery.add { await work() }
await nursery.add {
let first: Int = await try Task.withGroup(resultType: Int.self) { group in
await group.add { await work() }
await group.add { await work() }
await group.add {
do {
return await try boom()
} catch {
Expand All @@ -102,7 +102,7 @@ func first_ignoreFailures() async throws {
}

var result: Int = 0
while let v = await try nursery.next() {
while let v = await try group.next() {
result = v

if result != 0 {
Expand All @@ -117,9 +117,9 @@ func first_ignoreFailures() async throws {
}

// ==== ------------------------------------------------------------------------
// MARK: Advanced Custom Nursery Usage
// MARK: Advanced Custom Task Group Usage

func test_nursery_quorum_thenCancel() async {
func test_taskGroup_quorum_thenCancel() async {
// imitates a typical "gather quorum" routine that is typical in distributed systems programming
enum Vote {
case yay
Expand All @@ -137,19 +137,19 @@ func test_nursery_quorum_thenCancel() async {
///
/// - Returns: `true` iff `N/2 + 1` followers return `.yay`, `false` otherwise.
func gatherQuorum(followers: [Follower]) async -> Bool {
await try! Task.withNursery(resultType: Vote.self) { nursery in
await try! Task.withGroup(resultType: Vote.self) { group in
for follower in followers {
await nursery.add { await try follower.vote() }
await group.add { await try follower.vote() }
}

defer {
nursery.cancelAll()
group.cancelAll()
}

var yays: Int = 0
var nays: Int = 0
let quorum = Int(followers.count / 2) + 1
while let vote = await try nursery.next() {
while let vote = await try group.next() {
switch vote {
case .yay:
yays += 1
Expand Down