[Concurrency] Structured concurrency

Is the word "nursery" really appropriate here? I can see how there is some "child" relationship here, but this seems more like a concurrency scope of some sort.

Regarding the term nursery I was wondering if the word "nest" or "hub" would be a reasonable alternative.

Also, I'm relatively new to programming and new to swift. My perspective is purely naive and one of a beginner. I have a full time career in an unrelated field and I write apps only for the love and fun of it so that is my perspective.

My proposed terms feel more intuitive and approachable from a beginner / learner perspective.

Hmm maybe stupid question, I have probably missed some I’m sector of all the excellent posts that were presented today, but here we go...:

How do I create my first async method??? All the example seem to (have to) be async Because they consume other async methods - using await - but I gave not found one “entry “ async method. I.e just some method opening a huge file and parsing it according to some logic - which should be async.

3 Likes

The only one I found is Task.withUnsafe(Throwing)Continuation in this pitch.

Task.withUnsafeContinuation { continuation in
  ...
  if ... {
    continuation.resume(returning: returnValue)
  } else {
    continuation.resume(throwing: error)
  }
}
2 Likes

We'll be gradually adding async APIs to Swift, and there are already many in Apple's SDK. This pitch doesn't talk about them because it's explicitly just about the basic language mechanics.

3 Likes

Ok, I can see how you can "make it work" by adding a series of special cases to the type checker to handle things like this, but I don't understand how it works, and I still don't understand the motivation for adding something like this at this point in the concurrency design.

What is the static type of the value declared by the let? What can you pass it inout to? What happens when you capture it in a non-escaping closure? How does it compose with property wrappers?

-Chris

3 Likes

Yes. Something like this. But again, syntax is not important. Proposal describes async function results and async immutable variables. My concern is the lack of async function parameters and async properties. And I was thinking of moving async into a type system, because that's the most straightforward and comprehensive way of making something a first-class citizen, I could think of. But I guess using async as a parameter attribute would also work:

func boilVegetables(async veggies: [Vegetables]) async {
    let pan = Pan()
    await pan.boilWater()
    pan.addVeggies(await veggies)
    await pan.wait(.minutes(5))
}

Also, I should note that I was able to do this with current proposal, but solution is pretty verbose:

func makeDinner() async throws {
    await Task.withNursery(resultType: Void.self) { nursery in
        let veggies = nursery.addWithHandle {
            await try chopVegetables()
        }
        await boilVegetables(veggies: veggies)
    }
}
func boilVegetables(veggies: Task.Handle<[Vegetable]>) async {
    let pan = Pan()
    await pan.boilWater()
    pan.addVeggies(await veggies.get())
    await pan.wait(.minutes(5))
}
1 Like
  • The normal type that any other let would have, as inferred from its initializer or stated explicitly with a type annotation, the same way that the static type of an async call is the declared return type if the function. The asynchronicity is a modality checked separately from expression-typing, just like with error-throwing.
  • Nothing, because it’s a let.
  • It doesn’t, because it’s a let.
6 Likes

I imagine it would be hard to reason about this:

func f() async -> Int {
  async var x = 0
  x = 17 <---
}

How does this work? Could we await? Where would the keyword go? Would the compiler be smart enough to not start the async task, or would it have to let it run in case of side-effects? What happens if there's a later let y = await x * 3?

7 Likes

Scopes are typically implicit (i.e. usually there's no handle to them) so if the nursery wasn't passed as an argument I would agree that "task scope" would be the best name for the concept.

As it is though, inner closures can reference the nursery and add tasks to it, so I think nurseries are "task groups" really. It also feels like groups carry a lot less cognitive weight than scopes and most Swift programmers are already familiar with groups from libdispatch, which could help with adoption.

The distinction is minor and I guess I'm fine either way, but thought I'd throw this in for consideration.

1 Like

One of the use case of Detached tasks is to bridge the worlds between old & async world.
Current interface to get the result
public func get() async throws -> Success { ... }
makes it looks like we can only use it in another async function.
Maybe we can have another interface like
public func get(completion: (Success)->Void) throws -> Void to make the bridging feature more obvious?

There's a short discussion in the async/await thread. [Concurrency] Asynchronous functions - #62 by John_Lin

How would deadlines work? You pass them as regular parameters and then you manually throw an error if a deadline is not met? Would be nice if deadlines could be defined directly in the tasks suspension points and be propagated to the actor's executor which could then cancel the task and throw if the deadline is not met. The issue would be how to pass in this parameter to await. The await(.minutes(15)) below makes a lot of sense, conceptually, but syntax-wise is nonsensical.

func makeDinner() async throws -> Meal {
  async let veggies = chopVegetables()
  async let meat = marinateMeat()
  async let oven = preheatOven(temperature: 350)

  let dish = Dish(ingredients: try await(.minutes(15)) [veggies, meat])
  return await try oven.cook(dish, duration: .hours(3))
}

The first option I mentioned, and I think the only possible way given the design, would be something like.

func makeDinner() async throws -> Meal {
  async let veggies = chopVegetables(deadline: .minutes(5))
  async let meat = marinateMeat(deadline: .minutes(10))
  async let oven = preheatOven(temperature: 350, deadline: .minutes(15))

  let dish = Dish(ingredients: try await [veggies, meat])
  return try await oven.cook(dish, duration: .hours(3))
}

The issue I have with this is the disconnect between the deadline definitions and the actual action of waiting (the await suspension point). I believe the suspension point would be the best locus to define the deadline. Specially since the deadline of the task could have been defined transparently inside the function and you could have no way to control it. Not to mention the multiple deadline definitions which would be dominated by the smallest one. So you would have more math to do in your head when reading the code, to understand how the code actually behaves.

In other words, I see no mention of deadlines and I believe they should be deeply tied to the concept of tasks and executors. I can't see a way right now to make that work at a syntactic level. await(.minutes(15)) makes no sense at all, so this makes me think @Chris_Lattner3 is right to say we should maybe start with a library based solution and then, if it makes sense, add more syntactic sugar later on.

1 Like

The proposal has a section about deadlines which you might have missed.

I'm so happy to read this proposal, because I think it adopts exactly the correct approach. In my now-abandoned draft post to continue the earlier discussions on async (which I set aside until @John_McCall published this proposal), this is exactly the solution I was going to ask for, for "static" child tasks, right down to the async let syntax and semantics.

For the people who find async let inscrutable, I'd say that we already have most of the proposed structural pattern in Swift. Right now, I can write:

let answer: Int
… other calculations …
answer = try someCalculation()
result = answer + otherCalculation()

The answer variable is defined in one place, and initialized in another. Using the basic await/async, I could just as well write:

let answer: Int
… other calculations …
answer = await try someCalculation()
result = answer + otherCalculation()

However, this both starts and completes someCalculation at the last possible moment on the runtime timeline. To take advantage of concurrency, we want to be able to start someCalculation earlier on the runtime timeline:

async let answer = someCalculation()
… other calculations …
result = await try answer + otherCalculation()

This doesn't change any of the compiler logic about what type answer is. It also explains why there can't be an async var, because such variables can only be initialized, never assigned to.

I think this syntax will slightly surprising to users on first sight, but I think it's both explainable and understandable to less-experienced Swift users.

3 Likes

On the question of "dynamic" child tasks (aka nurseries), I'm basically OK with the proposal.

However, when I was thinking about this previously, I was actually thinking more in terms of (excuse the imprecise language) "async collections". That is, instead of a special nursery type, the programmer could choose to use any (mutable) collection type to accumulate the child tasks and their results.

This makes it easier for the programmer to iterate through and otherwise manage the tasks using information specific to the algorithm at hand, rather than having to reshape the algorithm to match the limited Collection-like behaviors of the nursery type.

Of course, this complicates the design problem, because it introduces questions about what is in the collection and when. (Ideally, the element type of the collection would be the result type of the tasks being created. Incomplete tasks can't exactly be added to the collection until they're completed.)

I'm not sure if @John_McCall and the other designers of this proposal think there is any value in considering some Collection-based approach, but I'm putting the idea out there for — I hope — a little consideration.

2 Likes

I think it would be straightforward to write a function that simply collects the results in a collection (in an arbitrary order? or in order of initial addition?). I don’t think we want to expose that as the primitive.

My point is that this would require both "nursery.add()" and "myCollection.append()" (or something), sorta-duplicating the housekeeping code for keeping track of the child tasks. It seems preferable to only have one thing to keep updated.

I think it's striking, in your chopVegetables nursery example, that dictionary semantics would be a cleaner way of tying the task to an index in the way you've shown. Or array semantics, to omit the explicitly stored index.

Basing this on Collection behaviors would also (I think) allow this sort of thing to be expressed using map (from a collection of items to be processed to a collection of processed items). That would be a big win for source code fluency, IMO.

(Admittedly, my objection is theoretical right now. I have code that's waiting for async/await to solve the complexity in simulating child tasks via completion handlers. I won't have a hands-on feel for the ergonomics of the proposed nurseries until I can rewrite that code.)

I’m saying that your idea seems implementable as a library function. That seems like a good library function to have, but I think it would impose a lot of overhead to make it the primitive operation.

OK, that sounds like a good place to start, at least.

Ideally, there might in the future be syntax support, maybe something like:

    let tasks: [async Int] = …

or:

    async let tasks: [async Int] = …

Such syntax could be added later, if it turns out to seem useful.

Anyway, this is not to take away from the proposal as it stands. It's already 150% of what I expected. I'm just being greedy for 200%. :slight_smile:

The spelling of async Int is Task.Handle<Int>, if you really want to build a collection of futures instead of a collection of values.

Well, I don't want to intrude further into this thread (I'll start a separate thread if there's anything more to say), but I think I really wanted to build a collection that was (apparently, in the source code) a collection of values, not a collection of futures — in the same way that async let creates a variable that's (apparently, in the source code) of the result value type, not a future.