For other suspension points what you're waiting on seems more intuitive.
With await chopVegetables() it's clear that you're waiting on the vegetables to finish being chopped, which is someone else's job.
It doesn't seem intuitive to me what you're waiting on with await Task.suspend(), and who's job it is to decide when that waiting is over.
Task.offerSuspension() feels you're willing to be suspended, while Task.suspend() seems like you want to be suspended. If you wanted to be suspended it should be up to you to communicate when you don't want to be suspended anymore via something like a hypothetical Task.resume() call.
This revision of the proposal is a HUGE step forward and addresses the significant concerns I had about previous drafts. All of the changes are major improvements. I am overall very supportive of this proposal, but have some remaining questions and an important suggestion for consideration (below).
Many thanks to the authors for their continued iteration on this bedrock proposal for Swift Concurrency.
This fits very nicely with Swift in general and is a key part of the new concurrency model. I am very excited and encouraged about this -- I think it will be a profound step and will cause ripples across the industry at large.
n/a
I've put a significant amount of effort into this proposal during the pitch phases, in the three prior iterations of the formal review, and with the Swift Concurrency effort in general. I read this draft in detail.
if executed within the scope of a specific actor function:
inherit the actor's execution context and run the task on its executor, rather than the global concurrent one,
the closure passed to Task {} becomes actor-isolated to that actor, allowing access to the actor-isolated state, including mutable properties and non-sendable values.
What does "specific actor function" mean in this case? Does that mean from within a method lexical defined on the actor type, or does this mean a function dynamically executing on an actor's executor?
If it is the former, this poses some potentially very surprising behavior, because refactoring a method in an actor out to a global function (or method on some other type) will cause a behavior change. This seems surprising and we should carefully consider this. If the refactored code would work naturally (because it is still on the current actors executor) then there is no concern here.
Implicitly eliminating references to self?
Related to the above question, Implicit "self" mentions this:
Closures passed to the Task initializer are not required to explicitly acknowledge capture of self with self. .... Note : The same applies to the closure passed to Task.detached and TaskGroup.addTask .
How is this implemented and why is special support required? Implicit vs explicit self has been widely discussed in the community and has pervasive impact on the language.
SE-0304 is a very large library proposal. I'd request that this syntactic sugar feature be split out to its own proposal, so we can see what the design space tradeoffs are, how the implementation works, and what it means for swift at large.
We also offer an asynchronous sleep function, which accepts the number of nanoseconds to > suspend for:
extension Task where Success == Never, Failure == Never {
public static func sleep(nanoseconds duration: UInt64) async throws { ... }
}
Thank you for adding the nanoseconds: label. I think this is well motivated and is a great step.
One further question: why is the duration a UInt64? Swift generally uses Int as the currency type for values like this, even those that are logically unsigned (e.g. the result of count). Shouldn't this be an Int?
Further naming concerns around launching tasks
The notes above mention:
Introducing a verb here is a huge step is a huge step forward, thank you!! However, I have a continued subtle but important concern about the naming applies for launching tasks that this proposal is using, and I still feel that an earlier version of the proposal had a better unifying approach.
To recap, this proposal provides three ways to launch a task:
Structured task groups: group.addTask { ... }
Unstructured attached tasks: let handle = Task { ... }
Unstructured detached tasks: let handle = Task.detach { ... }
I have concerns with each of these, and a simple suggestion for how to resolve it:
On the first, I am concerned that the verb in group.addTask { .. } is "add". This would make sense if TaskGroup were primarily a collection-like entity, but I don't think that is its primary function. I see it as an orchestrator of concurrent tasks which launches and tracks them (in an internal collection), managing and structuring their execution. The verb "add" is an implementation detail of how it maintains its internal collection, not the primary action that the client needs to think about.
On the second, I am concerned that the proposal uses an unlabeled initializer on a value type -- but its primary operation is to provide a global side effect of launching a new task. This leads to some surprisingly subtle code, e.g. see this post which suggests the best way to “fire and forget” some work off to the Main actor is:
Task { @MainActor in hello() }
While anything can be taught, this doesn't scream "I'm launching a new task that runs independently of the current one" because there is no verb. Furthermore, as the proposal points out, this requires the extremely unconventional use of @discardableResult on an init, something that is completely unprecedented in the Swift standard library. This isn't a thing in Swift because the initializer for a value type like this usually defines the initial condition for its value -- we don't (ab)use types for C++-like RAII things.
On the third, the verb detach doesn't convey the right action happening here and is inconsistent with other static members on Task. The other static methods on Taskapply to the current task. This operation doesn't detach the current task, it is launching a new task "that is detached" from the current one.
Furthermore, these are all launching tasks, but there is no unifying theme here. Cocoa design patterns generally want similar operations to have common roots (e.g. "method families") which this approach doesn't have.
My suggestion is to make a very minor tweak to the proposal, standardize of the compound verb phrase "launchTask", and use global functions for each of these. This would give the following design:
Structured task groups: group.launchTask { ... }
Unstructured attached tasks: let handle = launchTask { ... }
Unstructured detached tasks: let handle = launchDetachedTask { ... }
The simple fire and forget example above becomes:
launchTask { @MainActor in hello() }
which now has a verb in it. I think the introduction of a consistent verb pulls the family together and makes code using these operations much more clear. Of course, "launch" is just one suggestion, I would be equally happy with some other active verb (e.g. "run") applied consistently in this way.
Overall, this is a huge and important proposal which is a truly exciting step forward for Swift. The authors have put a tremendous amount of effort into fine tuning and refining this, and it really shows. Thank you to everyone working so hard on this!
Chris made a good point regarding Task { ... } lacking a verb in the current proposal. In fact, this initializer is analogous to the Cocoa's URLSession API IMO:
// Launching a new task
Task {
// Do something in new task
}
// Launching a new data task in some URLSession
let task = urlSession.dataTask(with: url)
task.resume()
As in the above snippet, the Task { ... } call site has an implicit launching behavior, while the existing URLSession API clearly separates the creation from launching. Adding a verb (like launchTask { ... } as suggested above) seems reasonable and would improve the use experience for the API.
So to throw out another alternative there (as I also think suspend is not as clear as yield was, but it depends on ones background or course, but suspend seems like a command):
Maybe we could make it explicit that the Task is actor-isolated:
Task.on(self) {…}
// or with the proposed name changes (which I like)
launchTask(on: self) {…}
// or
launchTask(isolatedTo: self) {…}
Alternatively we might require explicit capturing of self.
Another possibility might be something similar to @MainActor, to declare on which actor the closure is supposed to be run.
I think it makes perfect sense to add a task to a group, so I think group.addTask {} is great.
Task.detached {} also seems great to me, but I agree that a verb would be nice. How about Task.runDetached {} or Task.startDetached {}?
Task {} could perhaps be Task.run {} or Task.start {}?
I think it's a good idea to keep the naming different between starting structured and unstructured tasks. "Launch" feels a bit too heavy to me, like launching a new process.
I don't like the idea of free functions for 2 and 3. I like how both of these are within Task. I would suggest we either make both a parameterized initializer or both to use static factory functions.
// *parameterized initializer*
// default is .attached
let handle = Task(mode: .detached) { ... }
or
// *static factory functions*
// attached (launch|start|engage|)
let handle = Task.launch { ... }
// detached (unchanged from proposal)
I prefer the initializer approach. I don't mind the discarded results here.
edit: I imagined this can also be implemented as callable with out changing the user facing syntax but callable only applies to instances at the moment.
However, Swift's @Sendable closure checking has to be conservative, unless we give it special knowledge of task groups' semantics. We leave that to a later proposal.
Could you tell me the specific information about this "later propsal" ? When reading the proposal, I can not find any further information
It refers to a particular way of capturing in the previous paragraph,
it would theoretically be safe to allow them [child tasks] to mutate captured local variables, as long as every child task captures a disjoint set of variables [in the task group]
There's no related pitch as of yet. AFAICT, you'd need to at least be able to say that a concurrent closure is move-only and doesn't exit the declared scope, but that's for later (proposal).
Factoring out a closure already causes behavior changes, because closures are fairly sensitive to the context in which they are written:
You no longer get @Sendable
You can't get a non-escaping closure any other way
You don't have type context to resolve the parameter / result types if they aren't spelled out
Moreover, changes in actor isolation are rarely silent. If you actually use anything from the actor, you'll go from in-actor synchronous access to requiring await and it'll be clear what needs to be done with isolated parameters.
It's an underscored parameter attribute @_implicitSelfCapture. Its presence suppresses the requirement for explicit self capture.
Explicit self is there for a specific purpose: to help identify retain cycles by requiring one to explicitly call out self capture where it's likely to cause retain cycles. We intentionally did not make it required everywhere, and then later we removed explicit self in other unlikely scenarios. When capturing self in a task, you effectively need the task to have an infinite loop for that capture to be the cause of a reference cycle. That fits very well with the direction already set for explicit self.
It's an underscored attribute because we know we want this effect for the code running in a task, and that's why it's here in this proposal. It's not unlike the "marker protocol" for Sendable in that we know we want the effect, but we want more time to make it available for general use. We should have the discussion later about "dropping the underscore" from the attribute to make this a general feature that will help explicit self be meaningful, but we shouldn't make task creation worse while waiting for that discussion to happen.
This was already answered, but just to reiterate here: a 32-bit Int is too small when we're counting in nanoseconds.
A task group is a collection, though, which is an important part of the API contract. The way you get the result from a task that's been added to the task group is to get the values out of the task group using collection-like syntax (e.g., by iterating through the group with for...in). Something like group.runTask { ... } or group.launchTask { ... } emphasizes the the execution of the task, but it's the membership in the group that's the important action here. Hence, the "add" verb emphasizes the more important point.
What is the point of creating a Task if not to run it? This API is intentionally declarative---we are emphasizing that you are creating a task, and the task is associated with the provided function/closure.
It is conceivable that at some point we'll have the ability to create a task without running it, but that would be used much less often and would want to be called out very specifically, e.g., Task(.suspended) { ... }. That seems preferable to launchTask(.suspended) { ... } (it's not launched if it's suspended) or having to come up with another verb (createSuspendedTask { ... } or similar).
It's Task.detached { ... }, an adjective describing the result of the operation, just like RandomAccessCollection.sorted. Like the Task { ... } initializer, it describes what you're creating---and the primary reason to create a task is to run it, so we don't need to call that out repeatedly with "launch" everywhere.
To reiterate, I consider (1) to be a regression from group.addTask because it emphasizes the wrong thing, and (2)-(3) to be unnecessary verbosity as well as making "create a suspended task" more awkward should we add it in the future.
Is it so unlikely that someone will launch a task with an async loop over some infinite event observer? NotificationCenter appears to offer a convenient async sequence just for that now.
I think the main difference here is that RandomAccessCollection.sorted seems to have no side effects (it just created a new sorted collection). What Task.detached { ... } do is creating a detached task then run it.
And I disagree with the following statement:
We have a bunch of existing API that creates something and have a primary reason to use them, e.g. NSURLSessionTask and BGTaskRequest. They all follow the create-then-explicitly-use pattern. I don't see the point why we should add the implicit running semantics to the Task initializer.
P.S. I think both the global functions and scoped functions can be considered as candidates:
How does this work? How does a method (an initializer in this case) know if it is being invoked from the lexical context of an actor?
I don't understand what you mean. You obviously can't just move a closure from within an actor method to a global function if it refers to actor properties - you'd have to modify the closure.
Maybe I'm missing something. My understanding of what you're saying:
actor A {
func f() {
Task { ...stuff... }
}
}
will implicitly execute the task on the actors executor instead of the global queue, but that:
func globalfunc() {
Task { ... stuff ... }
}
actor A {
func f() {
globalfunc()
}
}
will execute the task on the global executor. Is this correct? How does this work, and why? It seems much more natural to follow the dynamic context from where a scope is launched. The current task of the second example is running on the actor's executor, so why wouldn't the subtask implicitly get that?
This behavior makes various appear to be pretty dangerous and seem very surprising. Why is it better than the dynamic behavior?
SE-0304 is a very large library proposal. I'd request that this syntactic sugar feature be split out to its own proposal, so we can see what the design space tradeoffs are, how the implementation works, and what it means for swift at large.
Your argument is that "this is warranted in this case", but I've seen no discussion about this behavior in the review thread, nor has there been any discussion about what this means for the language at large. This is a huge change (with a lot of prior discussions) to plop this into this proposal.
Why not run this as a separate syntactic sugar proposal? I don't see any downsides to handling it this way, it is completely unrelated to the rest of the proposal.