-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Structured concurrency: Updates from review #1 #1311
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
Conversation
214c64c
to
3744f75
Compare
} | ||
await group.add { | ||
meat = await marinateMeat() | ||
group.spawn { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I discuss the removal of await here a bit below.
The short version is:
- we never implemented this "suspend as a form of back-pressure" nor are we even confident it is the right thing to do, rather than some more holistic take on the concept.
- that await was tremendously confusing to reviewers -- "it is suspending until the operation completed?" was very frequently asked.
case .veggies(let v): veggies = v | ||
case .meat(let m): meat = m | ||
case .oven(let o): oven = o | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this had to use the enum dance due to the strong @Concurrent closure checking on main now
of childTaskResult: ChildTaskResult.Type, | ||
returning groupResultType: GroupResult.Type = GroupResult.self, | ||
body: (inout ThrowingTaskGroup<ChildTaskResult, Error>) async throws -> GroupResult | ||
) async rethrows -> GroupResult { ... } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note that implementation wise this is a bit of hell --- we have to duplicate the TaskGroup
struct impl twice, once normal and once Throwing
but there is no way around it.
It is the "if any closure submitted to spawn throws, then next() must be throwing" so we can't just rely on rethrows.
It is even more complex with the AsyncSequence conformance, so sadly we have to duplicate implementation a bit here.
I would also suggest doing: group.spawn -> Task.Spawned<T, Failure>
public struct Spawned {
/// Returns `true` if the task was successfully spawned in the task group,
/// `false` otherwise which means that the group was already cancelled and
/// refused to accept spawn a new child task.
public var successfully: Bool { handle != nil }
/// Task handle for the spawned task group child task,
/// or `nil` if it was not spawned successfully.
public let handle: Task.Handle<ChildTaskResult, Never>?
init(handle: Task.Handle<ChildTaskResult, Never>?) {
self.handle = handle
}
} since then it is just a single API to handle both:
|
d650ad3
to
e12d8f5
Compare
e12d8f5
to
dc6b7bc
Compare
/// Task handle for the spawned task group child task, | ||
/// or `nil` if it was not spawned successfully. | ||
public let handle: Task.Handle<ChildTaskResult, Never>? | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another alternative design for this is with throwing CancellationError
however then ONLY the withThrowingGroup
could do so, and we'd need some new name for trySpawn
(try spawn is terrible since it becomes try group.trySpawn { }
)
withTaskGroup(of: Int.self) { group in
// always spawns:
group.spawn {}
}
withThrowingTaskGroup(of: Int.self) { group in
// always spawns:
group.spawn {}
// spawns, unless the group/owning-task were cancelled, then it throws:
try group.spawnUnlessCancelled {}
}
So that'd be another option; and the spawn would be Void returning OR returning Task.Handle
and Task.Handle?
in the "may fail" case.
FYI @airspeedswift for when you have the time to review; plenty of various tradeoffs here, I hope I explained them well though, it is mostly:
- ability to not spawn in a cancelled group
- ability to always spawn (as async lets would do, if we express them as "implicitly in a group" which I think we should, but I did not manage to do this with my beginner SIL skill...)
- ability to get a handle for a specific spawned task
The Spawned addressess all in one API call though one has to search for it. The throwing versions are very annoying, and can only be available in withThrowingTaskGroup
. So there's some tradeoffs to make here I'd need input on. Thanks in advance!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can still spawnUnlessCancelled
inside a non-throwing group, you'd just need to handle the CancellationError
locally and not let it propagate out of the group block:
withTaskGroup(of: Int.self) { group in
do {
try group.spawnUnlessCancelled {}
} catch { print("couldn't spawn new task") }
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the error handling ceremony is too onerous, another option is to have spawnUnlessCancelled
return a Bool
like add
was previously proposed to, but remove the @discardableResult
from the declaration, so that you have to look at the result. I don't think it needs to return more information than a Bool
, though. That way you can use spawn
in the "don't care" case without worrying about implicit dropping of work you don't expect, and you can use spawnUnlessCancelled
when you do expect to stop adding work in the face of cancellation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great points, thank you! I'll adjust this right away so it is part of the review 👍
/// Returns `true` if the task was successfully spawned in the task group, | ||
/// `false` otherwise which means that the group was already cancelled and | ||
/// refused to accept spawn a new child task. | ||
public var successfully: Bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This of course is just var sucessfully: Bool { handle != nil }
so does not need storage)
I don't think that having |
Yeah I think you're right here... sounds like a not ignorable bool return sounds great here, thanks!
That's a great point, thanks that convienced me that indeed is an anti feature. |
Updates form first review