Swift Concurrency Roadmap

Will the corresponding chapters appear in the Swift book?

Yes, "The Swift Programming Language" will need to be updated for this new language feature. In general, updates to TSPL that describe a new language feature go out at the same time as Xcode ships with that feature.

This thread is about the development of language features and questions should be posed with that in mind. In general, you're not going to get answers to questions about Apple-specific frameworks on the forums (except possibly by way of example of how the language features might be used in practice).

So does the term of queues officially appear in the core of language? I did not read all thread, but first post

FWIW, I am pretty skeptical that implicit cancelation would scale and compose well. I think that explicit is more predictable and reasonable. There are strong analogies to the perils of trying to write exception safe code in C++ and pthread_cancel to learn from.

The counter argument is that await marking is already forcing you to think about suspension so it is ok to also think about cancelation on every await. I personally think that is a bridge too far -- people won't remember this, and lots of bugs will happen.

In my opinion, we should make async and throwing orthogonal from each other, and cancelation would only be possible in throwing contexts.

17 Likes

What about async functions that return Result?

Result

Edit:

writing_failable_asynchronous_api
https://developer.apple.com/documentation/swift/result/writing_failable_asynchronous_apis

If you’re interested, you could look into the experience of the Zio community. They are huge advocates of designing out the potential for bugs and this is the direction they went. As far as I know they don’t have any regrets about this choice.

I'm not sure what the question is, but I agree that async functions should be able to return result, and we probably want an async equivalent of Result that is Future like, for the same reason that we have both Result and try/throw.

-Chris

5 Likes

Is there a reason for async to be after the function name, as in func refreshPlayers() async { … }, that I’ve failed to notice?

In my opinion, putting async before func (i.e. async func refreshPlayers() { … }) would make it impossible to overlook, and improve code readability in general: e.g. “asynchronous function refreshPlayers()” reads as English, while “function refreshPlayers() asynchronous” does not.

1 Like

One reason that's already been mentioned is that async (with await) is used — syntactically — a lot like throws (with try), and so there's some harmony in putting it in a similar syntactic position.

I'd also add that, although we toss around the phrase "asynchronous function", the function is not exactly executed asynchronously.

In fact, the function is started synchronously, and completes asynchronously. In that limited aspect, it makes sense to mark the result as asynchronous.

2 Likes

There would not be anything magic about an async function or actor returning a Result<S,F> -- you'd simply be returning that value (result) asynchronously.

We do not anticipate magically converting swift code that is an async function returning result into anything else. The story is different with existing Objective C APIs where the importer can do things, but since you mention Result I don't think you mean the obj-c interop story. The obj-c part has it's own proposal though if that's what you meant @masters3d.

The "future"-like type in this proposal is Task.Handle.

You can read more about in the [Concurrency] Structured concurrency - #125 by ktoso proposal (check out the github version for the latest wording: https://github.com/DougGregor/swift-evolution/blob/structured-concurrency/proposals/nnnn-structured-concurrency.md

Hi Konrad! So excited for the concurrency proposals and the roadmap. Looking forward to seeing the one about async handlers too.

With regards to Task.Handle: is it a worry that many projects might want to wrap this in a type called Future - giving the same situation that was present before Result was added to the standard library? If so, would it not make sense to create such a type up front (instead of Task.Handle) - as I think Chris Lattner is also suggesting above?

As I understand our intent with the naming here is to discourage the use of Task.Handles (or "futures"), as they are not "structured" (please read the structured concurrency proposal) and lead to runoff futures which are harder to reason about.

The model Swift is attempting to recommend and push towards is more about async lets and async functions, and less about "randomly spawn a bunch of futures and never await on them" which futures can be used for (they have no limitations basically, you can await on them, but don't have to). They are super useful sometimes, but are not meant to be the default go-to solution in Swift -- async lets and async functions are.

Thus, justifying the different name so existing users don't just carry over their style of just using Futures into the new world, without reevaluating. John or Doug can also comment on this, if I represented their ideas here well, but that's how I understand our intention here and thus are ok with Task.Handle as the name.

20 Likes

I think you've represented the point very well, thank you.

1 Like

Hi everybody,

My colleagues at Google (most notably @gribozavr, @saeta, and @pschuh) and I have been doing an in-depth review of these proposals and are ready to post some technical questions and suggestions for each one. This is the first. We are also working on a posting that discusses the overall direction and emphasis, but that may take a day or two more. Sorry this took so long and thanks for your patience; y'all have done an impressive amount of work here and it takes a while to absorb it all!

Best Regards,
Dave


Actor Isolation and The Second Phase

“We can categorize memory into a few groups:…”

  • There’s no mention of mutably-captured value semantic types. Is that intentional? What’s the plan here?
  • In order to “trust the programmer” to use unsafe memory correctly, don’t we need a definition of “correctly,” which we understand to be the role of a memory model?

First Phase: Basic Actor Isolation

  • “error: an actor cannot access another's mutable state”—Are the (mutable) properties of a let-bound actor property that is a class instance considered to be part of the actor's mutable state? I think not, and I think that will take some education to communicate.

Second Phase: Full Actor Isolation

  • “Even after the introduction of actors, there will still exist the possibility for race conditions, through global variables and values of reference type”—This leaves out value types that don’t have value semantics and, again, types that do have value semantics but have been mutably captured by closures. Is that intentional? What’s the plan here?
  • “Classes (and types that contain class references) will change from being from being [sic] ‘actor unsafe‘ by default to being ‘actor local’… compiler support for guaranteed correct "copy on write" types via a mutableIfUnique class type.”—The former statement made me concerned about whether we’d still be able to implement CoW with this model, and the latter made me wonder what you had in mind here. There’s no other mention of this mutableIfUnique class type in the proposals, so how this would be handled is a bit thin. Can you please spell this out in more detail?

Glossary of Concepts

  • “A synchronous function is the kind of function that Swift programmers are already used to: it runs to completion on a single thread, with no interleaving code except for any synchronous functions it calls.”—I think “with no interleaving code” glosses over _read and _modify accessors. Yes, they’re not released features but they are in the plan and part of the community’s mental model for how the language works, so these proposals should account for them.
  • “An async function that is currently running always knows the executor on which it's running.”—what does “knows” mean? I think anthropomorphizing makes this harder to understand. Can you spell this out?
  • I think “interleaved” or “interleaving” would be a more appropriate adjective to apply to executors that serialize their partial tasks than “exclusive.”
  • The whole relationship between actors and executors is a bit fuzzy for me as a reader. We never see a representation of “executor” in the type system and we’ve seen that actors “act as” executors and looking further, that executors can be “bound to” actors. Can this relationship be clarified?
7 Likes

The implementation has a sketched-out idea here, where we establish whether the closure in which it is captured may run concurrency with the place where the variable is captured, using similar rules to what's described at https://github.com/DougGregor/swift-evolution/blob/actors/proposals/nnnn-actors.md#closures-and-local-functions. It needs a bit more work to make it into the proposal text.

No. A low-level memory model can come along later without affecting anything in the stated roadmap.

This is a topic of discussion in the various actor isolation threads. The roadmap document isn't the place to nit-pick wording.

As you know, this is being discussed on another thread. In the roadmap, we did not seek to nail down the distinction between "value semantics" and "value types", because it is not necessary for concurrency.

This is intentionally in the "later phase" because it isn't core to the model and doesn't need to be designed now. I can't imagine a world in which we would design @mutableIfUnique to support CoW types and... not leave us in a place where we can't implement CoW types?

_read and _modify don't interleave. Just because property accessors and async functions both use coroutines as a low-level implementation mechanism does not imply that they are the same, and I think you're confusing them here.

An async function always has access to the Task it is running in.

That's flipping the default for the terminology, which is doable but doesn't actually change anything in the model. Maybe it makes sense if we change the defaults around interleaving, which is a subject of the actors thread.

Yes, we can do that in one of the proposal revisions.

Doug

2 Likes

Please explain how this works rather than just claiming we don’t need to do anything about it. Without a definition of “correct;” how can I know whether I’m doing things correctly?

A complete definition of the memory model of Swift is out of scope for this effort.

Doug

Hi all,

First of all, I want to say that the amount of work that has gone into these proposals is absolutely awesome. Concurrency is a difficult and important topic, and the proposals show a lot of care has been invested. Thank you for the huge effort.

Within the S4TF and other Swift teams at Google, we’ve been digesting these proposals together. While we have quite a few technical questions and suggestions about the proposals, we wanted to post those separately, and use this post for more qualitative feedback about the overall direction and emphasis. The thoughts below are heavily influenced by folks on those teams and other concurrency experts.

At a high level, although we have some questions around technical details of the structured concurrency and async-await proposals, we’re overall quite excited for those directions. These proposals look well thought out and appear to address important gaps in Swift. The actors proposal appears to do a good job learning from prior actor systems including Akka and Orleans. This is excellent work by the proposal authors.

That said, the very central role of actors in the proposed system, and the complexity of the language changes that support them, concern us. Paradigms that wire together networks of interacting mutable objects are familiar, but—even without concurrency—tend to obscure a program’s data structures and algorithms. The result can be hard to scale, debug, and reason about. Using these object-oriented paradigms as the basis for concurrency—itself already a source of necessary complexity—compounds the problems.

I saw this effect play out in a past role (not at Google), where I used Akka actors within a production application to coordinate background work. Unfortunately, the anticipated benefits of actors never materialized, and the few benefits we could identify were far outweighed by complexity. Based on that experience, we never used Akka actors again within our Scala applications. Specifically, we found the system difficult to test, especially when a test needs to carefully arrange the state of multiple actors. We also found understanding the business logic difficult, because the actual algorithms were distributed across child actors and hidden behind message-passing ceremony.

Among modern languages, Swift has uniquely strong and accessible support for two simple but powerful tools: generic algorithms, and types with value semantics, which can be race- and deadlock-free by construction. Frameworks like SwiftUI demonstrate that these paradigms can be used to address traditional object-oriented domains while drastically reducing the chance of programmer error. Actors, by contrast, embody reference semantics. Although they define away the inherent concurrency problems of reference semantics (at the cost of significant language complexity) they do not address any of its other issues, even adding new ones due to re-entrancy. To open Swift’s support for concurrency with such a deep emphasis on reference semantics seems like a step in the wrong direction for the language.

Building on Swift’s strengths, we’d prefer to:

  • Address key gaps in Swift’s current fundamentals (e.g. closure captures of mutable values implicitly have reference semantics).
  • Allow asynchronous functions to be defined without having actors (as proposed it looks like the two features are mutually dependent).
  • Add structured concurrency.
  • Support first-class, composable, concurrent algorithms, which can be defined entirely in libraries

Even if actors turn out to be essential to the picture, we are concerned about the choice to make actors unconditionally reentrant. While this design choice defines away deadlocks, other languages make actors non-reentrant by default for a good reason: unlike data races, deadlocks are memory-safe and—given modern lock implementations—easy to debug. Conversely, the fact that actor state may shift unpredictably during awaited operations presents many of the classic hard-to-debug symptoms of race conditions.

We do appreciate the importance of interoperability with common code in the Swift and Objective-C ecosystems, and we see the obvious parallels between dispatch queues and actor message passing: actors are like serial dispatch queues with arbitrary attached state. That said, we don’t fully understand how the addition of this state serves dispatch-based use cases. In order to help the community evaluate actors versus alternative approaches, would it be possible for proposers to share more about the target use cases?

Finally, we’d be remiss if we didn’t mention that sharing mutable state across threads is still a thing. While we are 100% supportive of making happy-paths that use combinations of structured concurrency with value-semantic types or even actors, there exist important applications where neither actors nor raw use of value semantics can support implementations as efficient as those using locks (e.g. an in-memory database with multiversion concurrency control). We are concerned that ignoring efficient mutexes and locks in Swift’s concurrency model, or failing to think through the interactions, will limit Swift’s applicability to some problems.

Thank you, again, for everyone's hard work! Please do let me know if I’ve been confusing, or if you have further questions!

All the best,
-Brennan, on behalf of many!

23 Likes

I fully agree with the points and concerns raised.

I strongly hope that decision-makers will take this into account as well as other excellent community opinions.

Regards,

Van

1 Like