SE-0304 (4th review): Structured Concurrency

It's the lexical context.

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.

Doug

7 Likes