SE-0304: Structured Concurrency

Squeaking in under the wire for comments on this proposal. Structured concurrency is going to be a key part of the concurrency story for Swift, and I'm excited to see it come to the review stage.

Overall, I worry that the proposal--both text and underlying design being described by the text--is not as straightforward as it could be for such a key concept. I have read this proposal several times, and nothing compares to the clarity or lightbulb moment I had when reading Notes on structured concurrency, which distilled the entire concept of structured concurrency to the following:

Structured concurrency is to go statements (or, spawning a new thread or task) as control flow statements are to goto statements.

While concurrency itself is necessarily hard, the concept enabled here is not. Even as it's been said that these facilities introduced here are to be low-level primitives on top of which more "sugary" syntax can be provided, I think it's still very important to work towards as much clarity as possible here: users should be able to hold in their minds a good understanding of the basics and not have to rely on the more "sugary" syntax just to know what's going on.

I'm going to split my comments into several consecutive posts. The first will be a series of notes on clarity of writing. I've approached my reading of the proposal afresh from the perspective that it needs to be able to function serviceably as an expository text on the feature, and I believe strongly that clarity of writing will help promote clarity of thinking, and in turn clarity of design. Following that, I will try to outline some thoughts on the underlying design being described by the text.

9 Likes

Clarity-of-writing suggestions

A suspended task has more work to do but is not currently running.

  • It may be schedulable, meaning that it’s ready to run and is just waiting for the system to instruct a thread to begin executing it,
  • or it may be waiting on some external event before it can become schedulable.

The terminology is misinterpretable (note the use of the word “waiting” to describe both statuses). It would be ideal if we found some more intuitive dichotomy here.

Note that, when an asynchronous function calls another asynchronous function, we say that the calling function is suspended, but that doesn’t mean the entire task is suspended. From the perspective of the function, it is suspended, waiting for the call to return. From the perspective of the task, it may have continued running in the callee, or it may have been suspended in order to, say, change to a different execution context.

The “it” in the last two sentences refer to different things and are confusable. It is unclear what benefit there is to describing “perspectives" as opposed to writing: "The function is suspended, waiting for the call to return, whereas the task may have continued running in the callee, or..."

An asynchronous function that is currently running always knows the executor that it's running on.

“Knows” is confusing here because a function both certainly cannot "know" anything in the literal sense and also, in Swift, is not a data type, nor can it be extended to have members that could be called to return the value of something it's said to "know." Some clarification as to the nature of the knowing and what, specifically, knows (presumably “the system” and not exactly “the function”).

In some situations the priority of a task must be escalated in order to avoid a priority inversion:

  • If a task is running on behalf of an actor, and a higher-priority task is enqueued on the actor, the task may temporarily run at the priority of the higher-priority task. This does not affect child tasks or the reported priority; it is a property of the thread running the task, not the task itself.

The last clause is on its face contradictory with the preceding sentence. If the priority of a task can be escalated, then the priority can't not be a property of the task itself. That text is not necessary to make the point that the reported priority of the task doesn’t match the actual priority in that circumstance, which is plenty clear from the preceding text, and it’s unclear where the thread-but-not-the-task language comes into play.

It is possible to get a Task out of an UnsafeCurrentTask

This partial or complete sentence ends the section without punctuation. Is it intentional to leave this here? In what way is it possible, and what is the use case for it?

Task priorities are set on task creation (e.g., Task.runDetached or Task.Group.add) and can be escalated later, e.g., if a higher-priority task waits on the task handle of a lower-priority task.

This text is reduplicated.

Getting the handle's task allows us to check if the work we're about to wait on perhaps was already cancelled (by calling handle.task.isCancelled), or query at what priority the task is executing.

The cancellation example does not make sense as a justification for task: a prior paragraph notes that handle.isCancelled is a way to determine if a task has been canceled, so why handle.task.isCancelled here?

This ties into an overarching design question--more later on this.

For tasks that want to react immediately to cancellation (rather than, say, waiting until a cancellation error propagates upward), one can install a cancellation handler:

This could use some clarification: Does this API create a child task, install a cancellation handler on the current task, or create a detached task?

While all APIs defined on Task so far have been instance functions and properties, this implies that in order to query them one first would have to obtain a Task object.

By this point in the text, if I recall correctly, actually very few have been instance functions and properties; many have been static functions and properties, raising the question naturally in the reader's mind of why a Task object needs to be obtainable at all.

With this pattern, if a single task throws an error, the error will be propagated out of the body function and the task group itself. To handle errors from individual tasks, one can use a do-catch block or the nextResult() method.

Please provide an example to illustrate where one might need one or the other behavior.

8 Likes

Design comments

I do want to echo the overall feeling that @kiel expresses above:

I have seen this proposal go through multiple revisions to work through subtle issues related to ergonomics, but I can't shake the overall impression that the end result is more complex than it needs to be. I worry that it's not merely an aesthetic issue but ultimately a usability issue, where we will have solved many sophisticated pain points in very targeted ways, but with an end result that is substantially harder for users coming to the language.

To recap a bit, I'm coming at this from the perspective that, while concurrency is hard, the concept of structured concurrency ("it provides primitives with more structure than just spawning a new thread, like control flow statements provide more structure than goto statements; in so doing it helps to reduce a whole slew of bugs") is easy to understand, and we ought to be able to present some of that simplicity to users in the resulting API.

Task

I notice that Task has undergone several revisions, and in particular since the second pitch, instances of Task are now available--even though storing it for too long is not advised. Once I found the post explaining why this change was being made, I could sympathize, but the end result remains, I think, convoluted:

Near as I can tell, Task wears multiple hats:

  • It is a namespace for task-related functions that might otherwise be global functions (and indeed, the Task.with*Continuation functions were split out from this proposal and actually made global functions).
  • It functions like a stand-in for the current task in APIs such as Task.isCancelled; this design was essentially obligatory prior to the existence of instances of type Task.
  • It is an actual type, of which instances now exist, which in turn also have a substantially parallel set of instance method APIs--and not by accident but for reasons that are explainable.

I am not aware of any other type which has this kind of design in Swift, and again I do get the motivation for how it got there.

However, the practical effect is that users--before they write a single line of code that makes use of structured concurrency--learn that one can check if a task is canceled, for example, using static or instance methods found on Task, UnsafeCurrentTask, and/or Task.Handle. (Similarly, the text suggests that Task be Equatable for reasons seemingly redundant given that Task.Handle is already Equatable.)

I would strongly suggest jettisoning the use of Task as a namespace, and as much as possible to find a principled way to deduplicate static and instance methods that serve the sample purpose. If there are performance concerns because one or the other might be more easily optimized, I think those are properly addressed by compiler smarts rather than surfacing two different sets of APIs, which kind of solves the issue only by making it the user's problem.

UnsafeCurrentTask

The proposal states:

The UnsafeCurrentTask is purposefully named unsafe as it may expose APIs which can only be invoked safely from within task itself, and would exhibit undefined behavior if used from another task. It is therefore unsafe to store and "use later" an UnsafeCurrentTask.

Elsewhere, Swift provides with* APIs to expose something that shouldn’t be stored and “used later.” Was such a design considered instead of unsafeCurrent? Is there some consideration which prevents such a design here?

Task.Handle

I understand the impetus behind a design that tries not to be "future-forward" staying away from the name Future. To be clear, this is not a critique aimed at advocating for a more prominent future-based design, and I am totally supportive of the idea that the structured concurrency facilities here should not push such a future-based design.

However, even today in the proposal text itself, Task.Handle is still explicated for the user in at least one place as a future. If the easiest, most succinct way of teaching users about Task.Handle is to say that it's Swift's equivalent of a future (as has been described to users not merely in the proposal but also on this forum), it does not make sense to me to try to pessimize the naming of a type.

To preserve the intention of futures taking a backseat in this design, there are other tools in the API design toolbox; we can look, for example, to the precedent of Result deliberately vending a limited API for the purposes that it's meant to serve while making clear that it's not meant to be an alternative to supplant throwing functions.

For the same reason, I think the primitive Task.runDetached (as have thought others here on the forums) can have a more succinct name also. Yes, it is a primitive that we're not trying to put forward as the go-to option in concurrency (pun intended). But neither does that mean that we need to namespace and pessimize the naming of it. It would seem to me that a global function merely named run, detach, spawn, launch, or anything similar would work. If we're worried that structured concurrency facilities can't compete with that sort of name, then we should optimize the design of the rest of the facilities, not pessimize this one.

I guess the point here is that I think it would be a feature, not a bug, if someone who solely wants to spawn an unstructured concurrent job doesn't need to spell out Task: they're not making use of the structured concurrency features.

Task.Group

First, some bikeshedding comments:

I agree that Group is more fitting than Nursery, which is admittedly an odd though precedented name. However, there are some drawbacks with Group, and with the namespacing it under Task:

In general, namespacing implies a sort of "big-endian" is-a or has-a relationship -- Unicode has many Encodings, and UTF8 is one such Encoding. This isn't the relationship being described here: a Group isn't a kind of Task, and while it's true that a Task can have many Groups, the much more salient point we're emphasizing here (the "structure" in structured concurrency) is that a Group is something that has many Tasks.

I will also say that, unfortunately, a "group" lacks the the connotations of structure that we're going for. Notably, a major point of the structured concurrency feature is that subtasks aren't supposed to outlive the group; by contrast, in plain English, a group (of people, things, tasks) can be disbanded but the individual elements of that group carry on existing.

Therefore, I would suggest not namespacing task groups under Task, and I would look toward a name that could perhaps capture the underlying point about its role as an aggregate of tasks that cannot outlive the group: something like Agenda, Kanban, etc.


Now, returning to @kiel's thought about the use sites which are at the heart of this proposal: creating new groups and adding subtasks. Whatever the shortcomings of going literally with @kiel's illustrative code, I think the underlying impetus for simplification should be considered seriously:

let (vegetables, meat, oven) = Task {
        Task { try await chopVegetables() }
        Task { try await marinateMeat() }
        Task { try await preheatOven(temperature: 350) }
    }

By contrast, the proposal proposes a withGroup design, stating:

Task.Group has no public initializers; instead, an instance of Task.Group is passed in to the body function of withGroup. This instance should not be copied out of the body function, because doing so can break the child task structure.

My question here would be based on our prior design of Task: Why provide an instance at all? Why not emulate the Task APIs in providing static APIs, which might in turn call an “unsafe current task group” internal API underneath? Then, group.add could be Group.add (or, if we go with detach for Task.runDetached, perhaps this could just be named attach), and there would be nothing that relies on developer discipline not to copy an instance of Group that shouldn't be copied.

I'd also like to point out some nits regarding details of the task group design:

By default, the task group will schedule child tasks added to the group on the default global concurrent executor. The startingChildTasksOn argument can be provided to override this behavior.

Why does this API need to differ from the preceding API in the text, clarifying that it’s child tasks that are started on an executor, rather than just spelling the argument label just startingOn? Can anything but a task be scheduled on an executor, and could the task group be scheduling anything but its child tasks?

The add operation returns true to indicate that the task was added to the group, and false otherwise. The only circumstance under which the task will not be added to the group is when the task group has been cancelled; see the section on task group cancellation for more information.

What is the rationale for returning true or false (an unusual way to signal failure in Swift) versus, say, not returning a value but throwing CancellationError?

Miscellaneous

Longstanding convention in U.S. English, and specifically observed in past Apple style guides, is that the word is spelled "canceled" with one "l" (while "cancellation" is always spelled with two).

I understand there was some attempt to implement this in the APIs here, but it was reverted pending further discussion on review. I think we need to establish a convention in Swift (perhaps it could be added to the API naming guidelines) of whether to observe or deviate from this, and then we need to stick to it.

Finally, there appears to be some inconsistent use of the word run versus operation for argument labels for closures; it's not a big deal, but I would imagine the goal here is to be consistent. Although it probably will not be spelled out very often, I would still think run, being more concise and evocative, would be the preferred here.

18 Likes

Task.new {} is a better option than Task.runDetached {}, it means create a new Task which is different than current running one and also not a child task confined in.

2 Likes

Nice, thanks everyone for the good round of feedback :+1:

I'll try to address the main ideas:

I don't think that was seriously considered to be honest, but yeah it could be a way to go. Depends on the core team what they'll prefer here.

A bit wording nitpicking but okey I can wordsmith this more.

"The async function always knows" is a shortcut for "an async function is guaranteed to be able to obtain an Executor reference by some internal means, unlike a synchronous function which may or may not be running within an asynchronous context and be able to get such executor (or Task)".

I'll wordsmith this bit a little.

It's a missing . here.

As a matter of composability; since the type Task exists, and some API may want it, and I have obtained it using the unsafe API, I should absolutely be able to invoke some function that made the decision to accept a Task. Banning this would just invite for trouble and make Task less of a first class citizen. Such function is probably pretty silly and one should not really write such, but it is safe and legal to declare, so we must play well with being able to declare and invoke them when it makes sense, thus the UnsafeCurrentTask -> Task conversion is important (besides being trivial).

That's an entire discussion by itself, if that instance is needed or not, but I think we've argued long enough that yes, but very rarely the task instance may be useful – I can reiterate here again, but I see you agree with it in other comments.

I suspect a search/replace happened here... Yeah the recommended API here is meant to be handle.isCancelled - fixed.

The handle.task was one of those that people asked for because "why not, and it's trivial to have" since the handle keeps the task anyway. I can imagine some use cases using the Task as a key in some datastructure etc, but yeah it's just future proofing.

It does not launch / spawn new task. Only group.add and Task.runDetached are ways to spawn tasks today. Hopefully we can adjust those names a little, no-one loves the group.add for example -- maybe we'll end up with spawn as the word? For spawning tasks :slight_smile:

I'll clarify in the proposal. You can notice that by the operation not being concurrent nor escaping, but yes we should spell it out.

I mean, this is very specifically talking about the instance functions defined so far (priority, checkCancellation, isCancelled) and explains how the unsafe task API enables the safe static usable from any context counterparts for them.

The word "all" is incorrect but the sentence and premise is correct: up until this point in the proposal there was no defined way in the detailed design to call those APIs from a synchronous function, thus we introduce the usage of the unsafe API to implement the safe static counterparts.

I'll improve the wording a little bit, but the structure seems right to me.

Sure I can add a small note; though I don't think we should have to go in depth to designing all kinds of concurrency operators here; One example would be a form of "gather first 2 successful results out of those 5 child tasks; regardless if any other tasks fail." I'll add some wording on it, including the snippet:

func gather(first m: Int, of work: [Work]) async throws -> [WorkResult] { 
  assert(m <= work.count) 
  
  return Task.withGroup(resultType: WorkResult.self) { group in 
    for w in work { 
      await group.add { await w.doIt() } // spawn child tasks to perform the work
    }  
    
    var results: [WorkResult] = []
    while results.count <= m { 
      switch try await group.nextResult() { 
      case nil:             return results
      case .success(let r): results.append(r)
      case .failure(let e): print("Ignore error: \(e)")
      }
    }
  }
}

PR with updates structured-concurrency: improve wording and add examples by ktoso · Pull Request #1305 · apple/swift-evolution · GitHub

1 Like

Continuing on API notes then... commenting here on behalf of myself, so others or the core team may disagree – we'll see :slight_smile:

I hear you loud and clear on this one. It's really too bad that async let and anything with "nicer to look at syntax" was decided to be punted out of this proposal and into a future one; as really the groups API here is really not what most people will be interacting with at all on a daily basis, unless building your own concurrency "operator" like a scatter-gather or "race" or similar.

So I feel a bit "hands tied" on that – as we said we'll leave the nice syntax and niceties to a future proposal, but let's go through one by one:


Right, yes those did become top level, though mostly due to a lot of prior art with the various withUnsafe... functions that already exist and these fitting the same style. Nor are they really about Task (much less than all the other ones in this proposal).

I feel this one really boils down to personal style and opinions a lot... As a person I do like the Task namespace a lot, but I'm just one of the authors and by far not the one with most influence on it :wink: I'll leave that up to the core team to decide as it's a general Swift library style guideline thing.

The reason the namespace is nice is because it's easy to discover similar APIs I might need - e.g. this way I might learn about Task.local etc.


I don't think there's any technical reason other than "yet again more nesting".
Usages of this are likely to be deep in framework and library code so it may not be a "huge" issue.

We had hoped to have @unmovable at some point during the initial drafts... which would disallow escaping/moving the value out of the current scope, meaning that the closure would not really be necessary.

I'll leave that to @John_McCall or @Douglas_Gregor to comment if that's a likely future direction or not.


I'm not sure I agree with this; It is not only "pessimizing" the name, but also setting expectations correctly: Looking at this as a "language immigrant", coming from another language (e.g. Scala, Ruby, JS, Java, Kotlin...), or even just Swift with pervasive PromiseKit, NIO or Combine's Future types seeing a Future in Swift would trigger the "I know how to use this, map(), flatMap(), right?" and then disappointment (and maybe finally enlightenment?) :wink:

I think it is useful to call this a Task.Handle because it will not lead people into cargo culting on "okey, instead of NIO Future we pass now Swift Future" but rather lead into rethinking things a little bit more -- one would hope.

Maybe there's a middle ground here?

I–personally–would for example like if we took two verbs and hyper focused on using them throughout the APIs, e.g.: group.spawn (child task) and Task.detach and if any other forms of child tasks were allowed to be spawned we'd also talk about them using spawn. If any other "not child" tasks were allowed to be spawned, we'd use the verb "detach".

I would definitely not want to use Task.launch or Task.new as the detach operation. It looks too inconspicuous but is actually a disaster for priority and tracing propagation. So it has to "shout" that "I am going to abandon all information from my parent task!!!"


As I said before, this really feels like an opinion / personal preference thing to be honest, so let's see what the core team comes up with. Yeah it could be either Task.Group or TaskGroup doesn't really matter.

I will say however that there definitely will be new "concurrency container thingy" types in the future. So whatever we do here affects future APIs like Task.DetachedLifecycleAttachedToClassContainer or similar types.


I believe Joe's idea here was to spell out that it is not the group that will run on any other executor, which the following spelling might imply to the reader: withGroup(startingOn: coolExecutor) vs. the boringly explicit withGroup(startingChildTasksOn: coolExecutor). We had a number of people be confused if the group itself is a task or not etc. (in fact, it used to be a child task, but isn't anymore).

The actual proper solution IMHO would be soft-faults here...

I've been hoping for those for a long long time already: [stdlib] Cleanup callback for fatal Swift errors - #4 by ktoso

The lack of panics or similar mechanisms in Swift and the very explicit nature of Errors forces us into these weird API decisions... Specifically this is about: It is not the operation that might "fail", it is the infrastructure that might. The same appears in networking, or other panics vs errors situations...

But we're not going to get panics / soft faults any time soon I guess. So this design is being forced to think about -- is this automatic cancellation case worth the syntactic noise of having to try every single group.add even though the vast majority of time those will never throw...?


Hah yes, there was a PR and was reverted... :slight_smile: There seems to be both prior art for disregarding the language rules in Apple APIs e.g. NSOperation (regardless of it being wrong linguistically...). Also some people expressed that they feel super strongly about it, so indeed the current and expected spelling is cancel, cancelled, cancellation - as weird as it it.

2 Likes

Thank you @xwu for writing this. This captures my general feeling about this proposal much better than I could have. :clap:

I too feel like I understand the general design direction ("structured concurrency is to threads what structured programming was to goto"), and the high-level conceptual model ("a hierarchical tree of tasks where all child tasks of a task must be completed before that task can complete"). But when I read the detailed design and the proposed API, that understanding falls apart. Is Group a type of Task? Can tasks have children that aren’t in a Group? If I need to add the child tasks to the group, how do I add the group to the current task?

I can’t speak for @xwu, but for me this is not a question about "nicer to look at syntax". This is a question of the API not expressing a clear conceptual model that is easy to understand. async let will not make the conceptual model clearer – at best it will hide it so that I don’t have to think about it. (That might also be a worthwhile goal, but it’s a different goal.)

(Edit: I actually had an even harder time understanding the proposal before async let was removed from it.)

7 Likes

Quick clarity of presentation issue: “detached tasks” are referenced in the Task Priority section before they’re defined.
—————-
I agree about the conceptual clarity piece. If the key concept is a parent->child tree, then the naming could do a better job of reinforcing that concept. I’m thinking things like withChildGroup instead of withGroup.

I also wonder if “detached” is the right way of thinking about tasks that run to completion even if there are no remaining uses of their task handle. We could just as easily conceptualize it as a child that can outlive its parent. Then everything is a child (aiding with conceptual clarity) and whether it respects parent cancellation is just a property on the child.
———-
I’ll second what others have said above about deeply appreciating the body of work that has been put forth. Thanks all.

1 Like

Great post and great suggestions Xiaodi. While it didn't occur to me while reading the proposal initially, now that you point it out, I agree completely that the word Task is getting over loaded here.

I am personally in favor of the verb "spawn" given its extensive use in concurrency based world, and the relative ambiguity of the word "run". I think we should align the nomenclature around that and christen it as a term of art.

You're point is very good here with runDetatched. Given a word like spawn, we could use global functions instead of artificially namespacing them:

let dinnerHandle = spawnDetached {
  try await makeDinner()
}

which also provides an evocative and aligned syntax for the "async let" equivalent thing:

let dinnerHandle = spawn {
  try await makeDinner()
}

and would plug in nicely to the task group API.

-Chris

7 Likes

I share the same concerns as @xwu here. I feel like the actual simplicity of the model is lost in the proposal text and that that makes it in turn harder to understand why this might be such a good idea. It also ends up feeling like this is such an advanced feature that should mostly be used by library developers to abstract it away for end users. When I read that same article before the sea of concurrency proposals for swift even began, the idea that stuck with me is that this seems like such a good and fundamental idea that it should almost be as ubiquitous as a for, if, or other fundamental statements, but that is not the general feeling I get when reading this proposal, it seems heavier.

3 Likes

Yeah I think spawn is a fine word for these :+1: spawnDetached also seems like the right "it should be verbosely warning about the detach" criterium I have about it in my mind :slight_smile: The group would group.spawn { ... } which also reads nice.


For the thread: some people were commenting / questioning the utility of Equatable/Hashable on the Task type.

I just wanted to mention that right now I had a number of internal teams reach out and ask for things where that equatability "saved the day" and provides better diagnosis for abuse of APIs etc. So I definitely think it is very valuable and did pop up in the real world already.

2 Likes

I really like the general direction of this proposal.

The only thing I would really like to see being added though is the possibility to state on Task.Group.add/spawn whether the child task is required to have stopped for the group to be able to stop. This would be the default. But some tasks could opt out of this strong behavior and will get cancelled instead when the strong tasks have all stopped.

This makes it really easy to have child tasks which run concurrently to the others but don’t determine the temporal dimension of the group. Imagine a child task in the running Demo to play music while cooking or control the lights while doing so etc. These tasks should not prevent the group from finishing but just get cancelled on group end.

That is an interesting thought. Quick thought : it might also be a way to handle timeouts.

Well said, and I agree with @hisekaldma's point here completely. A clear conceptual model can only increase in clarity the more we remove non-essential "sugar" from the mix. That taking away async let has made the proposal potentially harder for users to understand is a symptom and not a cause: it points to an underlying problem with the design of what is included.


The bottom two responses, from @ktoso and @hisekaldma, both vividly demonstrate exactly why the naming does matter and why it's absolutely not a matter of "style" or "preference."

To reiterate: nesting implies a hierarchy of is-a or has-a relationships. Is an Encoding.UTF8 a kind of Encoding? Yes. Is a Task.Group a kind of Task? No. That Joe feels compelled to clarify this confusion for a specific argument label is a sign that something is not right here. This has absolutely nothing to do with one's opinion about namespaces as an API naming stylistic choice.

Consider how this confusion is instantly clarified by (a) not namespacing; and (b) using a more evocative name: Is an Agenda a kind of Task? Self-evidently not: if it were, it'd be a nested type, a subclass, or conform to some common protocol.


This doesn't "set expectations"; it's really a misdirection. If a user expects a "future-based" concurrency design, this choice merely divorces those expectations from the Task.Handle type, but they'll still expect or want a Future type just as much. It cuts them off from applying existing understanding or experience to Swift (including their knowledge of the shortcomings of a future-based concurrency design).

If you came over to my house expecting a dinner feast and I had only potato chips, I could "set expectations" by pretending that there's no food in my house, but I bet you'd be disappointed if you saw me eating potato chips afterwards. And by telling you there's no food at all instead of just telling you about the snacks, I've deprived you of the opportunity to decide if all you want to have are snacks anyway, or if you'd like to order your own food.


I also feel very strongly about it: As a Canadian, I'd spell it NSColour too, but that's simply not how we spell things in Swift. All APIs in the Swift standard library adhere to standard U.S. English conventions, and the rules regarding the doubling of consonants before "-ed" and "-ing" (which differ from international English rules) are no exception. The burden is on those who wish to diverge from these rules to explain why, in this setting, they should not apply. A good reason might be that as a term of art something is always spelled in a nonstandard way (e.g., "HTTP referer"), but I'm aware of no such case for "canceled," which meanwhile is a term gaining cultural currency.

6 Likes

I think I was pretty clear, but to clarify who the convincing should be directed at: the core team specifically requested the current spelling, and we reverted my change that aligned the APIs with with proper US grammar. I very much agree that proper grammar spellings would be the right thing to do, but this was met with strong opinions and explicit request to revert.

So yeah, for what it is worth: very much agreed on the spellings on a personal level at least.

+1 on the general direction, it might still need a bit of bikeshedding

Definitely: callback hell is real and it burns.

Mostly yes, it looks like is a good ground for awesome features and code.
More below.

No, only played with the concurrency preview in swift.

A quick read of the last version of the proposal. But I thoroughly read previous ones, and I have followed concurrency proposal and threads (pun not intended) for some time.


My own preferences are for spawnDetached, wether namespaces or not, and group.spawn.

I agree that having both static and instance methods is pretty confusing. And I believe instance methods are better. Maybe we could have the following extension, removing the need for static methods, or would it just be a different kind of confusing?

extension Optional where Wrapped == Task {
    var isCancelled: Bool { wrapped?.isCancelled ?? false }
}

https://developer.apple.com/documentation/foundation/urlerror/2293052-cancelled

(where the documentation for the URLError.cancelled property says "An asynchronous load has been canceled")

1 Like

Nice. When will we be able to bikeshed async let? :grin: I think the "async" in "async let" is a different kind of async... And perhaps the thread about "async let" could inspire the naming of spawnDetached()?

I think as you can "await" (the result of) a function:

let dinner = await makeDinner()

it could make sense to be able to "defer" getting the result until the end of the current scope at the latest:

let dinnerHandle = defer makeDinner()

So I would propose changing async let x = y to let x = defer y.

Alternatively

let dinnerHandle = makeDinner() on the side

:grin:

1 Like

I don't recall this case personally, but I want to make a general observation:

The core team is made up of people, and people make mistakes (at least, I certainly do!). The core team highly values arguments based on agreed-on principles that keep the language and APIs more consistent. If a call was made that detracts from that based on a principled argument, then we should be open to discussing it and potentially revising a decision - we shouldn't just go with "the core team says so".

That said, we shouldn't open or reopen every debate - there are many things where we "just need to make a call", and there isn't a strong principle that guides one answer vs the other. In those cases, it is really important that the core team exists to just pick a winner: arguing endlessly about would just generate heat but not light.

One other thing that is important to distinguish: there is a difference between the individual humans that sit on the core team (e.g. me) who make suggestions or have requests during the design and iteration of a proposal, and the final official "judgement" made at the end of a proposal review. Guidance from the individuals can be useful to help shape things in ways that make it more likely to work based on their experience, but it is really the core team as a whole that makes the final judgement.

-Chris

8 Likes

Yes, a strong task would define the lifetime of the group and the weak tasks would follow it.

This distinction between strong and weak tasks will make structured concurrency so much more versatile.

Terms of Service

Privacy Policy

Cookie Policy