[Concurrency] async/await + actors


(Chris Lattner) #1

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

-Chris


(Chris Lattner) #2

Oh, also, one relatively short term piece of this model is a proposal for adding an async/await model to Swift (in the form of general coroutine support). Joe Groff and I wrote up a proposal for this, here:
https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619

and I have a PR with the first half of the implementation here:
https://github.com/apple/swift/pull/11501

The piece that is missing is code generation support.

-Chris

···

On Aug 17, 2017, at 3:24 PM, Chris Lattner <clattner@nondot.org> wrote:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782


(Brent Royal-Gordon) #3

I think you're selecting the right approaches and nailing many of the details, but I have a lot of questions and thoughts. A few notes before I start:

* I only did one pass through this, so I probably missed or misunderstood some things. Sorry.
* I think the document may have evolved since I started reading, so some of this may be out of date.
* I haven't yet read the rest of the thread—this email is already long enough.
* I have a lot of experience with Cocoa-style callback-based concurrency, a little bit (unfortunately) with Javascript Promises, and basically none with async/await. I've never worked with a language that formally supported actors, although I've used similar patterns in Swift and Objective-C.

# async/await

I like the choice of async/await, and I agree that it's pretty much where mainstream languages have ended up. But there are a few things you seem to gloss over. You may simply have decided those details were too specific for such a sweeping manifesto, but I wanted to point them out in case you missed them.

## Dispatching back to the original queue

You correctly identify one of the problems with completion blocks as being that you can't tell which queue the completion will run on, but I don't think you actually discuss a solution to that in the async/await section. Do you think async/await can solve that? How? Does GCD even have the primitives needed? (`dispatch_get_current_queue()` was deprecated long ago and has never been available in Swift.)

Or do you see this as the province of actors? If so, how does that work? Does every piece of code inherently run inside one actor or another? Or does the concurrency system only get you on the right queue if you explicitly use actors? Can arbitrary classes "belong to" an actor, so that e.g. callbacks into a view controller inherently go to the main queue/actor?

(If you *do* need actors to get sensible queue behavior, I'm not the biggest fan of that; we could really use that kind of thing in many, many places that aren't actors.)

## Delayed `await`

Most languages I've seen with async/await seem to allow you to delay the `await` call to do parallel work, but I don't see anything similar in your examples. Do you envision that happening? What's the type of the intermediate value, and what can you do with it? Can you return it to a caller?

## Error handling

Do you imagine that `throws` and `async` would be orthogonal to one another? If so, I suspect that we could benefit from adding typed `throws` and making `Never` a subtype of `Error`, which would allow us to handle this through the generics system.

(Also, I notice that a fire-and-forget message can be thought of as an `async` method returning `Never`, even though the computation *does* terminate eventually. I'm not sure how to handle that, though)

## Legacy interop

Another big topic I don't see discussed much is interop with existing APIs. I think it's really important that we expose existing completion-based Cocoa APIs with async/await. This ideally means automatic translation, much like we did with errors. Moreover, I think we probably need to apply this translation to Swift 4 libraries when you're using them from Swift 5+ (assuming this makes Swift 5).

## Implementation

The legacy interop requirement tends to lean towards a particular model where `await` calls are literally translated into completion blocks passed to the original function. But there are other options, like generating a wrapper that translates calls with completions into calls returning promises, and `await` is translated into a promise call. Or we could do proper continuations, but as I understand it, that has impacts further up the call stack, so I'm not sure how you'd make that work when some of the calls on the stack are from other languages.

# Actors

I haven't used actors before, but they look like a really promising model, much better than Go's channels. I do have a couple of concerns, though.

## Interop, again

There are a few actor-like types in the frameworks—the WatchKit UI classes are the clearest examples—but I'm not quite worried about them. What I'm more concerned with is how this might interoperate with Cocoa delegates. Certain APIs, like `URLSession`, either take a delegate and queue or take a delegate and call it on arbitrary queues; these seem like excellent candidates for actor-ization, especially when the calls are all one-way. But that means we need to be able to create "actor protocols" or something. It's also hard to square with the common Cocoa (anti?)pattern of implementing delegate protocols on a controller—you would want that controller to also be an actor.

I don't have any specific answers here—I just wanted to point this out as something we should consider in our actor design.

## Value-type annotation

The big problem I see with your `ValueSemantical` protocol is that developers are very likely to abuse it. If there's a magic "let this get passed into actors" switch, programmers will flip it for types that don't really qualify; we don't want that switch to have too many other effects. I also worry that the type behavior of a protocol is a bad fit for `ValueSemantical`. Retroactive conformance to `ValueSemantical` is almost certain to be an unprincipled hack; subclasses can very easily lose the value-semantic behavior of their superclasses, but almost certainly can't have value semantics unless their superclasses do. And yet having `ValueSemantical` conformance somehow be uninherited would destroy Liskov substitutability.

One answer might be to narrow the scope of the annotation: Don't think of it as indicating that it's a value type, merely think of it as a "passable-to-`Actor`s" protocol. I'll call this alternate design `Actable` to distinguish it from "is a value type". It's not an unprincipled hack to retroactively conform a type to `Actable`—you're not stating an intrinsic property of your type, just telling the actor system how to pass it. It's totally coherent to have a subclass of a non-`Actable` class add `Actable` and require its own subclasses to be `Actable`. And we can still synthesize `Actable` on structs and enums.

A middle ground would be to define the protocol as being for types which can be safely passed to another thread—`Shareable`, say. That might even permit implementations that used atomics or mutexes to protect a shared instance.

(Sorry if this comes off as bikeshedding. What I'm trying to say is, while the exact name is unimportant, the semantic we want the protocol to represent *is* important. I suspect that "has value semantics" is too broad and will lead users into misbehavior.)

## Plain old classes

In the section on actors, you suggest that actors can either be a variant of classes or a new fundamental type, but one option you don't seem to probe is that actors could simply *be* subclasses of an `Actor` class:

  class Storage: Actor {
    func fetchPerson(with uuid: UUID) async throws -> Person? {
      ...
    }
  }

You might be able to use different concurrency backends by using different base classes (`GCDActor` vs. `DillActor` vs. whatever), although that would have the drawback of tightly coupling an actor class to its backend. Perhaps `Actor` could instead be a generic class which took an `ActorBackend` type parameter; subclasses could either fix that parameter (`Actor<DispatchQueue>`) or expose it to their users.

Another possibility doesn't involve subclasses at all. In this model, an actor is created by an `init() async` initializer. An async initializer on `Foo` returns an instance of type `Foo.Async`, an implicitly created pseudo-class which contains only the `async` members of `Foo`.

  class Storage {
    let context: NSManagedObjectContext
    
    init(url: URL) async throws {
      // ...build a Core Data stack...
      context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
      context.persistentStoreCoordinator = coordinator
    }
    
    func fetchPerson(with uuid: UUID) async throws -> Person? {
      let req = NSFetchRequest<NSManagedObject>(entityType: "Person")
      req.predicate = NSPredicate(format: "uuid = %@", uuid)
      req.fetchLimit = 1
      
      return execute(req, for: Person.self).first
    }
    
    func execute<R: RecordConvertible>(_ req: NSFetchRequest<NSManagedObject>, for type: R.Type) throws -> [R] {
      let records = try context.fetch(req)
      return try records.map { try Person.init(record: $0 as! NSManagedObject) }
    }
  }
  
  let store: Storage.Async = await try Storage(url: url)
  
  // This is okay because `fetchPerson(with:)` is `async`.
  let person = await try store.fetchPerson(with: personID)
  
  // This is an error because `execute(_:for:)` is not `async`,
  // so it's not exposed through `Storage.Async`.
  let people = try store.execute(req, for: Person.self)

A third possibility is to think of the actor as a sort of proxy wrapper around a (more) synchronous class, which exposes only `actor`-annotated members and wraps calls to them in serialization logic. This would require some sort of language feature to make transparent wrappers, though. This design would allow the user, instead of the actor, to select a "backend" for it, so an iOS app could use `GCDActor<Storage>` while its server backend could use `DillActor<Storage>`. (`Storage` is a bad example for shared code, but you get the idea.)

My point here is simply that, although you show the actor-ness of a type as being fundamental to it, I'm not sure it needs to be.

### Lifting parameter type restrictions into `async`

The major downside of an "actors are not special types" model is that it wouldn't enforce the parameter type restrictions. One solution would be to apply those restrictions to *all* `async` functions—their parameters would all have to conform to the magic "okay for actors" protocol (well, it'd be "okay for async" now). That strikes me as a pretty sane restriction, since the shared-state problems we want to avoid with actors are also questionable with other async calls.

However, this would move the design of the magic protocol forward in the schedule, and might delay the deployment of async/await. If we *want* these restrictions on all async calls, that might be worth it, but if not, that's a problem.

We'd probably also need to provide an escape hatch—either a function-wide `async(unsafelyShared)` annotation, or a per-parameter `@unsafelyShared` attribute.

## Function-typed parameters

You mention that function types would be unsafe to pass "because it could close over arbitrary actor-local data", but closures over non-shared data would be fine. Another carve-out that I *think* we could support is `async` functions in general, because if they were closures, they could close over their original actor and run inside it. This might be able to subsume the "closure over non-shared data" case.

## The inevitable need for metadata

GCD started with a very simple model: you put blocks on a queue and the queue runs them in order. This was much more lightweight than `NSOperationQueue`, which had a lot of extra stuff for canceling operations, prioritizing them, etc. Unfortunately, within a few years Apple decided that GCD *needed* to be able to cancel and prioritize operations, so they had to pack this information into weird pseudo-block objects. In Swift, this manifested as the `DispatchWorkItem` class.

My point is, in anything that involves background processing, you always end up needing more configurability than you think at the start. We should anticipate this in our design and have a plan for how we'll attach metadata to actor messages, even if we don't implement that feature right away. Because we'll surely need to sooner or later.

## Examples

In a previous section, I used a class called `Storage` as an actor; I think that might be a good type to illustrate with. I envision this as a type that translates between the Swift structs/enums you use in your model layer and the REST server/SQLite database/Core Data stack/CloudKit database you use to actually store it.

Other examples might include:

* A shared cache:

   actor SharedCache<Key: Hashable, Value> {
       private var values: [Key: Value]

       actor func cachedValue(for key: Key, orMake makeValue: (Key) async throws -> Value) rethrows -> Value {
           if let value = values[key] {
               return value
           }

           let value = try await makeValue(key)
           values[key] = value
           return value
       }
   }

* A spell checker:

   actor SpellChecker {
       private let words: Set<String>

       actor func addWord(_ word: String) throws {
    words.insert(word)
    await save()
  }
       actor func removeWord(_ word: String) throws {
    words.remove(word)
    await save()
  }
  
  func save() async throws { ... }

       actor func checkText(_ text: String) -> Checker {
           return Check(words: words, text: text, startIndex: text.startIndex)
       }

       actor Checker /* Hmm, can we get a SequenceActor and `for await` loop? */ {
           fileprivate let words: Set<String>
           fileprivate let text: String
           fileprivate var startIndex: String.Index

           actor func next() -> Misspelling? { ... }
       }

       struct Misspelling: ValueSemantical {
           var substring: Substring
           var corrections: [String]
       }
   }

# Reliability

Overall, I like reliability at the actor level; it seems like an appropriate unit of trap-resistance.

I don't think we should incorporate traps into normal error-handling mechanisms; that is, I don't think resilient actors should throw on traps. When an invariant is violated within an actor, that means *something went wrong* in a way that wasn't anticipated. The mistake may be completely internal to the actor in question, but it may also have stemmed from invalid data passed into it—data which may be present in other parts of the system. In other words, I don't think we should think of reliable actors as a way to normalize trapping; we should think of it as a way to mitigate the damage caused by a trap, to trap gracefully. Failure handlers encourage the thinking we want; throwing errors encourages the opposite.

To that end, I think failure handlers are the right approach. I also think we should make it clear that, once a failure handler is called, there is no saving the process—it is *going* to crash eventually. Maybe failure handlers are `Never`-returning functions, or maybe we simply make it clear that we're going to call `fatalError` after the failure handler runs, but in either case, a failure handler is a point of no return.

(In theory, a failure handler could keep things going by pulling some ridiculous shenanigans, like re-entering the runloop. We could try to prevent that with a time limit on failure handlers, but that seems like overengineering.)

I have a few points of confusion about failure handlers, though:

1. Who sets up a failure handler? The actor that might fail, or the actor which owns that actor?

2. Can there be multiple failure handlers?

3. When does a failure handler get invoked? Is it queued like a normal message, or does it run immediately? If the latter, and if it runs in the context of an outside actor, how do we deal with the fact that invariants might not currently hold?

# Distributed actors

I love the feature set you envision here, but I have two major criticisms.

## Heterogeneity is the rule

Swift everywhere is a fine idea, but heterogeneity is the reality. It's the reality today and it will probably be the reality in twenty years. A magic "distributed actor" model isn't going to do us much good if it doesn't work when the actor behind it is implemented in Node, PHP, or Java.

That means that we should expect most distributed actors to be wrappers around marshaling code. Dealing with things like XPC or Neo-Distributed Objects is great, but we also need to think about "distributed actors" based on `JSONEncoder`, `URLSession`, and some custom glue code to stick them together. That's probably most of what we'll end up doing.

## It's just a tweaked backend

You describe this as a `distributed` keyword, but I don't think the keyword actually adds much. I don't think there's a simple, binary distinction between distributed and non-distributed actors. Rather, there are a variety of actor "backends"—some in-process, some in-machine, some in-network—which vary in two dimensions:

1. **Is the backend inherently error-prone?** Basically, should actor methods that normally are not `throws` be exposed as `throws` methods because the backend itself is expected to introduce errors in the normal course of operation?

2. **How strictly does the backend constrain the types of parameters you can pass?** In-process, anything that can be safely used by multiple threads is fine. In-machine, it needs to be `Codable` or support `mmap`ing. In-network, it needs to be `Codable`. But that's only the common case, of course! A simple in-machine backend might not support `mmap`; a sophisticated in-network backend might allow you to pass one of your `Actor`s to the other side (where calls would be sent back the other way).

Handling these two dimensions of variation basically requires new protocol features. For the error issue, we basically need typed `throws`, `Never` as a universal subtype (or at least a universal subtype of all `Error`s), and an operation equivalent to `#commonSupertype(BackendError, MethodError)`. For the type-constraining issue, we need an "associated protocol" feature that allows you to constrain `ActorBackend`'s parameters to a protocol specified by the conforming type. And, y'know, a way to reject actor/backend combinations that aren't compatible.

···

On Aug 17, 2017, at 3:24 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

--
Brent Royal-Gordon
Architechies


(Michel Fortin) #4

Great writeup. Here's a few comments about the manifesto, actors and value semantics specifically.

# About actors and data isolation

Can I call global functions like sin(x) in an actor? Surely you need to be able to do that. But how can the compiler know which functions are safe to use and which are out of bound for an actor?

Actors shouldn't access the global mutable state and thus should only call functions that do not access the global mutable state. We probably need a concept of a functions being pure (as in not accessing the global mutable state) for that.

# About the part "Does a type provide proper value semantics?"

I would argue that this is the wrong question. Most types are a hybrid form of value and reference semantics. All you need is to limit what you do to what is proper value semantics and forbid the parts that use reference semantics. Otherwise you are pessimising the capabilities of the types.

For instance, has Array<UIView> value semantics? You might be tempted to say that it does not because it contains class references, but in reality that depends on what you do with those UIViews. If you treat the class references as opaque pointers (never dereferencing them), you preserve value semantics. You can count the elements, shuffle them, all without dereferencing the UIViews it contains. Value semantics only end when you dereference the class references. And even then, there are some exceptions.

I suggested a little while ago on this list some principles based on this that would allow for the compiler to enforce value semantics with a `pure` attribute and I'm currently working on a draft proposal. In essence this proposal will have pure functions guaranteeing value semantics and no access to the global state, and a correct implementation for copy-on-write types. I think this would be useful for actors.

···

Le 17 août 2017 à 18:24, Chris Lattner via swift-evolution <swift-evolution@swift.org> a écrit :

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Michel Fortin
https://michelf.ca


(Yuta Koshizawa) #5

I think we also need `reasync` like `rethrows`.

    extension Sequence {
        func map<T>(_ transform: (Element) async throws -> T) reasync
rethrows -> [T] { ... }
    }

    let urls: [URL] = ...
    let foos: [Foo] = await try urls.map { await try downloadFoo($0) }

···

--
Yuta


(Andre) #6

Hi Chris,

This looks amazing(!) … I am really looking forward to the end result whatever that may be, because I know it will be awesome.
Im really excited and a lot raced though my mind while I read it...

···

———————————————
Part 1: Async/await

    let dataResource = await loadWebResource("dataprofile.txt")

Was there any thought about introducing the concept of a timeout when awaiting?

Something like an `await for:` optional parameter?
Then, if used with try, it could go like…

    let dataResource = try await for:10/*seconds*/ loadWebResource("dataprofile.txt”) catch _ as Timeout { //abort,retry cancel }

Timeouts should probably be handled at a higher level, but its just something that jumped out to me a little, since its something I notice sometimes people neglect to take care of… :confused:

———————————————
Part 2: Actors

One other thing that also jumped at me was, if we are going to have actors, and having them encapsulate/isolate state, would it also make sense to make sure that we can’t invoke state changing messages when its invalid?

As an example, if we had a ”downloader” actor that downloads multiple files, we wouldn't want to be able to send invalid messages such as `begin` when downloading has already ”begun”…. Would it then make more sense to have callable messages be determined by the publicly visible state of the actor instead?

For example, if the downloader actor hasn’t begin downloading, then the only available messages are `begin` and `addItem`, conversely if the actor is ”downloading” then the only messages it should accept are `cancel` and `getProgress`…

Im thinking something along the lines of merging a class with an enum I suppose…
Just put a gist here if you want to see what I am thinking: https://gist.github.com/andrekandore/f2539a74002d1255cfc3da58faf0f007

It may add complexity but I think (at least for me) instead of writing a lot of boilerplate, it would come naturally from the "state contract"… and it could be more safe than manually doing it myself….

Anyways, maybe Im wrong, but just something I thought about…

———————————————

I would appreciate to hear what you think. :slight_smile:

Cheers,

Andre

H29/08/18 7:25、Chris Lattner via swift-evolution <swift-evolution@swift.org>のメール:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Elviro Rocca) #7

Thanks a lot for this piece: it was a great read, that could serve as primer to most discussions about async programming in general, also thanks to the various links and references.

Here's my thoughts:

- You might have been too soft on the callback-based syntax of many Cocoa APIs: those are really bad, and in general a pain to use. Of course a import strategy for future Swift will be needed, but I wouldn't consider design constraints exclusively derived from some Cocoa APIs shortcomings, like URLSession.dataTask(withURL:completionHandler:) that I think can be safely ignored.

- I agree on the focus on async/await as compiler-level tools for defining and using coroutines, with Future<T> and stuff like that to be considered as library functions built on top of the syntax: this way async/await doesn't become mere syntactic sugar.

- I really STRONGLY disagree on conflating async and throws: they are different things, and exist for different purposes, the same way as Future<T> and Result<T> are different things and should be usable separately. The right way to handle a "failable" future is to simply use a Future<Result<T>>, and eventually define convenience functions for the "success" and "failure" cases, or event methods on Future (in this case a ResultType protocol would be needed). It's a lot better to define simpler concepts that compose in interesting ways, rather than conflating unrelated things for minor conveniences.

- async/await is good, but the Actor model part is simply glorious :smiley:

- I'm so happy that adding a native idiomatic Actor model has been considered for Swift, and I like the interplay with await for making actor functions return, as well as the progressive disclosure that can be achieved with more and more restrictive keywords: it even seems to me a real step-up from existing implementations of the model.

- in general I think that one of the best (if not the best) features of Swift is the idea of progressive disclosure, and that should be preserved... and I'm a fan of "actor class" :smiley:

- I like the "ValueSemantical" protocol idea, but eventually I would like Swift to have features to actually enforce safe patterns though the language itself, like a "pure" or "safe" keyword: I almost use no classes (excluding the UIKit ones that I'm forced to use) and mostly write pure functions, but as the language forces me to do extra work when I'm dealing with an Optional or a throwing function, and that's a good thing in my opinion and facilitates my work as a software engineer, I would love a language that warned me that I'm accessing a potentially mutable instance with reference semantics in a context that I'm mistakenly considering as pure, or that the function I'm writing is not actually pure because a non-pure function was called inside it.

- I love the way the "actor" keyword for a method transforms it in an "inbox": I think the implementation you're proposing is super "Swifty" and could appear so convenient that many people could be drawn to the language just thanks to the great native concurrency model; I think that a powerful but simple and usable concurrency model is a heavy motivation for adopting a certain language in many contexts.

Thanks

Elviro

···

Il giorno 18 ago 2017, alle ore 00:24, Chris Lattner via swift-evolution <swift-evolution@swift.org> ha scritto:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Yuta Koshizawa) #8

Hi, I have a question about the proposed `async/await`.

Are `throws async` and `await try` allowed? I think we have three options.

1. allows only `async throws`-`try await`
2. allows both `async throws`-`try await` and `throws async`-`await try`
and does not distinguish them
3. allows both and distinguishes them like `Promise<Result<T>>` and
`Result<Promise<T>>`

Although 3 is the most expressive, I think it is too complicated. In most
cases we need only something similar to `Promise<Result<T>>`. To select 1
also makes it possible to support 3 in the future. So 1 seems a good choice
to me.

···

--
Yuta


(Georgios Moschovitis) #9

I am wondering, am I the only one that *strongly* prefers `yield` over `await`?

Superficially, `await` seems like the standard term, but given the fact that the proposal is about coroutines, I think `yield` is actually the proper name. Also, subjectively, it sounds much better/elegant to me!

-g.


(Benjamin Spratling) #10

Howdy,

It’s good to have a formal, language supported way to manage these aspects of concurrency.

In particular, I like that it establishes the asynchronously provided values into the surrounding scope, like guard. This helps me write a clean sequence of calls with access to values which ‘skip’ a call. Often I need to make a few asynchronous calls which fetch disparate value types from disparate endpoints or frameworks and then provide them all together into some final API.

I also like that the actor model formally wraps up access to shared resources. It seems using these together, the compiler would be able to do some fundamental reasoning about whether deadlocks could occur (or preventing them entirely without expensive run-time mechanisms) and also determine if certain code paths were forgotten. One question I have is “how do we assign queues or priorities to the async methods?” I had been toying with the idea of declaring a reference to the queue on which async methods run at their definition of the method, but this actor model groups around the data which which needs coordination instead, which is possibly better.

I haven’t yet read the philosophy links, so if I’m repeating ideas, or these ideas are moot in light of something, I guess ignore me, and I’ll just feel slightly embarrassed later.

However, at the UI level we often don’t even use the GCD methods because they do not directly support cancelation and progress reporting, so we use NSOperation and NSOperationQueue instead. To me, adding a language feature which gets ignored when I build any full-featured app won’t be worth the time. Whatever is done here, we need to be cognizant that someone will implement another layer on top which does provide progress reporting and cancellation and make sure there’s a clean point of entry, much like writing the Integer protocols didn’t implement arbitrary-sized integers, but gave various implementations a common interface.

I know that data parallelism is out of scope, but GPU’s have been mentioned. When I write code which processes large amounts of data (whether that’s scientific data analysis or exporting audio or video tracks), it invariably uses many different hardware resources, files, GPU’s and others. The biggest unsolved system-level problem I see (besides the inter-process APIs mentioned) is a way to effectively “pull” data through the system, instead of the current “push”-oriented API’s. With push, we send tasks to queues. Perhaps a resource, like reading data from a 1000 files, is slower than the later stage, like using the GPU to perform optimal feature detection in each file. So my code runs fine when executed. However, later I add just a slightly slower GPU task, now my files fill up memory faster than my GPU’s drain it, and instead of everything running fine, my app exhausts memory and the entire process crashes. Sure I can create a semaphore to read only the “next" file into memory at a time, but I suppose that’s my point. Instead of getting to focus on my task of analyzing several GB’s of data, I’m spending time worrying about creating a pull asynchronous architecture. I don’t know whether formal “pull” could be in scope for this next phase, but let’s learn from the problem of the deprecated “current queue” function in GCD which created a fundamental impossibility of writing run-time safe “dispatch_sync” calls, and provide at least the hooks into the system-detected available compute resources. (If “get_current_queue” had provided a list of the stack of the queues, it would have been usable.)

Together with the preceding topic is the idea of cancellation of long-running processes. Maybe that’s because the user needs to board the train and doesn’t want to continue draining power while exporting a large video document they could export later. Or maybe it’s because the processing for this frame of the live stream of whatever is taking too long and is going to get dropped. But here we are again, expressing dependencies and cancellation, like the high level frameworks.

I’m glad someone brought up NSURLSessionDataTask, because it provides a more realistic window into the kinds of features we’re currently using. If we don’t design a system which improves this use, then I question whether we’ve made real progress.

NSBlockOperation was all but useless to me, since it didn’t provide a reference to ‘self’ when its block got called, I had to write a subclass which did so that it could ask itself if it had been cancelled to implement cancellation of long-running tasks. NSOperation also lacks a clean way to pass data from one block to those dependent on it. So there’s lots of subclasses to write to handle that in a generic way. I feel like block-based undo methods understood this when they added the separate references to weakly-held targets. That enabled me to use the framework methods out of the box.

So my point is not that these problems need to be solved at the language level, but that we aren’t spending a large amount of time designing and implementing a glorious system which will be used inside some wrapper functions like an UnsafeMutableRawPointer (is that the right name, I can’t ever remember.), and then become useless once anyone starts building any real-world app, UI or server-side. UnsafeMutableRawPointer has its place, but for the vast majority of app developers, they’ll likely never use it, and thus it doesn’t deserve years of refining.

-Ben Spratling


(Philippe Hausler) #11

I have read over the proposal and already asked a few questions ahead of the email chain and I have some follow up points that are perhaps worth consideration.

First off, I think that async/await is a great concept! From personally using a similar concept in C#, I think it can be an approachable yet powerful tool to resolve a decent number of problems with threading. It is worth stating, however, that it does NOT solve all problems with threading, or at least, not on its own.

One thing I have in mind is that Swift, of course, does exist in a vacuum. It, by nature, is being used for writing iOS, watchOS and macOS applications to be shipped on those platforms. On those platforms, libdispatch is often used for things like completion handlers and similar (or indirectly used via NSOperationQueue). Of course there are a few of those devices that only have one core or are thermally constrained under load. It is then imperative that applications (at least under the hood) utilize appropriate quality of service to ensure that certain workloads do not get scheduled as much as others. For example: if an application is synchronizing some sort of state over the network, it may be the best choice to run that work at a low quality-of-service. In this example, if a specific piece of work is then blocked by the work that is running at a low quality-of-service, it needs to temporarily override that low quality-of-service to match the blocked work. I think that any concurrency model we consider should be able to work constructively with a situation like this, and take QoS into account.

For sake of argument, though: let's presume that completion handlers are always going to be appropriately scheduled for their quality-of-service. So why is the override important to think about? Well... in the cases of single core, or other cases where a ton of work in limiting the number of cores available, there can be a problem known as a priority inversion. If no override is made, the low priority work can be starved by the scheduling of a waiter. This results in a deadlock. Now you might of course think: "oh hey, that's DispatchSemaphore's responsibility, or pthread_condition_t" etc... Unfortunately semaphores or conditions do not have the appropriate information to convey this. To offer a solution to this problem, the start of the asynchronous work needs to be recorded for the threads involved to the end of that asynchronous work, then at the beginning of the waiting section an override needs to be created against those asynchronous threads and the override is ended at the point that it is done waiting.

In short, effectively just waiting on completion handler will cause severe performance problems - to resolve this it seems like we absolutely need to have more entry points to do the correct promotions of QoS.

What do you think is the best way of approaching a resolution for this potential pitfall?

···

On Aug 17, 2017, at 3:24 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Cavelle Benjamin) #12

Disclaimer: not an expert

Question
I didn’t see any where the async is required to time out after a certain time frame. I would think that we would want to specify both on the function declaration side as a default and on the function call side as a customization. That being said, the return time then becomes an optional given the timeout and the calling code would need to unwrap.

func loadWebResource(_ path: String) async -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async -> Image
func dewarpAndCleanupImage(_ i : Image) async -> Image

func processImageData1() async -> Image {
    let dataResource = await loadWebResource("dataprofile.txt")
    let imageResource = await loadWebResource("imagedata.dat")
    let imageTmp = await decodeImage(dataResource, imageResource)
    let imageResult = await dewarpAndCleanupImage(imageTmp)
    return imageResult
}

So the prior code becomes…

func loadWebResource(_ path: String) async(timeout: 1000) -> Resource?
func decodeImage(_ r1: Resource, _ r2: Resource) async -> Image?
func dewarpAndCleanupImage(_ i : Image) async -> Image?

func processImageData1() async -> Image? {
    let dataResource = guard let await loadWebResource("dataprofile.txt”) else { // handle timeout }
    let imageResource = guard let await(timeout: 100) loadWebResource("imagedata.dat”) else { // handle timeout }
    let imageTmp = await decodeImage(dataResource, imageResource)
    let imageResult = await dewarpAndCleanupImage(imageTmp)
    return imageResult
}

Given this structure, the return type of all async’s would be optionals with now 3 return types??

.continuation // suspends and picks back up
.value // these are the values we are looking for
.none // took too long, so you get nothing.

···

On 2017-Aug -17 (34), at 18:24, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Kenny Leung) #13

Hi All.

A point of clarification in this example:

func loadWebResource(_ path: String) async -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async -> Image
func dewarpAndCleanupImage(_ i : Image) async -> Image

func processImageData1() async -> Image {
    let dataResource = await loadWebResource("dataprofile.txt")
    let imageResource = await loadWebResource("imagedata.dat")
    let imageTmp = await decodeImage(dataResource, imageResource)
    let imageResult = await dewarpAndCleanupImage(imageTmp)
    return imageResult
}

Do these:

await loadWebResource("dataprofile.txt")
await loadWebResource("imagedata.dat")

happen in in parallel? If so, how can I make the second one wait on the first one? If not, how can I make them go in parallel?

Thanks!

-Kenny


(William Jon Shipley) #14

I’m curious about this statement at the end in "alternatives":

The proposed design eliminates the problem of calling an API (without knowing it is async) and getting a Future<T> back instead of the expected T result type. C# addresses this by suggesting that all async methods have their name be suffixed with Async, which is suboptimal.

Not that I necessarily think always returning Futures would be good (or bad), but it seems to me like since we’re required to use the “await” keyword now, we’d just have some similar keyword in an always-Future-returning universe, like:

let image = future makeImage()

-W


(William Jon Shipley) #15

I think the comments on “prettify()” are from an earlier version of the sample code? The way it’s called it doesn’t look like it’d access theList at all.

  actor TableModel {
    let mainActor : TheMainActor
    var theList : [String] = [] {
      didSet {
        mainActor.updateTableView(theList)
      }
    }
    
    init(mainActor: TheMainActor) { self.mainActor = mainActor }

    // this checks to see if all the entries in the list are capitalized:
    // if so, it capitalize the string before returning it to encourage
    // capitalization consistency in the list.
    func prettify(_ x : String) -> String {
      // ... details omitted, it just pokes theList directly ...
    }

    actor func add(entry: String) {
      theList.append(prettify(entry))
    }
  }


(Xiaodi Wu) #16

Hi Chris et al,

This is definitely a great initial model.

To clarify, are the authors proponents of the syntax shown in the body of
the draft--async, throws, async throws--or of the alternative design listed
below that is "probably the right set of tradeoffs"--async, throws,
async(nonthrowing)?

Naively, to me, the observation that in your introduction you've separated
`async` and `throws` suggests that keeping the two orthogonal to each other
is the more teachable design. I appreciate how there is an intimate
connection between `async` and `throws`, but it seems to me that
`async(nonthrowing)` is a highly unintuitive result--_especially_ since the
rationale for not outright making `async` a subtype of `throws` is that
asynchronous non-throwing functions are a key Cocoa idiom.

Other than that, I would hope that the primitive functions `beginAsync`,
etc., are indeed exposed outside the standard library; agree that their
names could use some light bikeshedding :slight_smile:

···

On Thu, Aug 17, 2017 at 5:25 PM, Chris Lattner via swift-evolution < swift-evolution@swift.org> wrote:

On Aug 17, 2017, at 3:24 PM, Chris Lattner <clattner@nondot.org> wrote:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions
for what concurrency should look like in Swift. This will surely be an
epic multi-year journey, but it is more important to find the right design
than to get there fast.

I’ve been advocating for a specific model involving async/await and actors
for many years now. Handwaving only goes so far, so some folks asked me to
write them down to make the discussion more helpful and concrete. While I
hope these ideas help push the discussion on concurrency forward, this
isn’t in any way meant to cut off other directions: in fact I hope it helps
give proponents of other designs a model to follow: a discussion giving
extensive rationale, combined with the long term story arc to show that the
features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear
comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

Oh, also, one relatively short term piece of this model is a proposal for
adding an async/await model to Swift (in the form of general coroutine
support). Joe Groff and I wrote up a proposal for this, here:
https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619

and I have a PR with the first half of the implementation here:
https://github.com/apple/swift/pull/11501

The piece that is missing is code generation support.


(Chris Lattner) #17

That is mentioned in Potential Future Directions at the end. It can certainly be done, but it comes with tradeoffs, and it seems better to settle on the design the basic proposal first.

-Chris

···

On Aug 17, 2017, at 7:28 PM, Yuta Koshizawa <koher@koherent.org> wrote:

I think we also need `reasync` like `rethrows`.

    extension Sequence {
        func map<T>(_ transform: (Element) async throws -> T) reasync rethrows -> [T] { ... }
    }

    let urls: [URL] = ...
    let foos: [Foo] = await try urls.map { await try downloadFoo($0) }


(Matthew Johnson) #18

Hi Chris,

This is fantastic! Thanks for taking the time to write down your thoughts. It’s exciting to get a glimpse at the (possible) road ahead.

In the manifesto you talk about restrictions on passing functions across an actor message. You didn’t discuss pure functions, presumably because Swift doesn’t have them yet. I imagine that if (hopefully when) Swift has compiler support for verifying pure functions these would also be safe to pass across an actor message. Is that correct?

The async / await proposal looks very nice. One minor syntax question - did you consider `async func` instead of placing `async` in the same syntactic location as `throws`? I can see arguments for both locations and am curious if you and Joe had any discussion about this.

One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.

Matthew

···

On Aug 17, 2017, at 5:25 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 17, 2017, at 3:24 PM, Chris Lattner <clattner@nondot.org <mailto:clattner@nondot.org>> wrote:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

Oh, also, one relatively short term piece of this model is a proposal for adding an async/await model to Swift (in the form of general coroutine support). Joe Groff and I wrote up a proposal for this, here:
https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619

and I have a PR with the first half of the implementation here:
https://github.com/apple/swift/pull/11501

The piece that is missing is code generation support.

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Félix Cloutier) #19

I'm glad to see this moving forward. I have a few questions and comments.

First, I'd like it if someone could clarify for me what beginAsync and suspendAsync "actually do", and how they interact with `async` functions. I'm throwing a lot of good will at this but my brain is still fairly imperatively-wired, so let me try to rephrase this in more procedural terms and let's see if I got anything wrong.

My understanding is that an `async` function takes its continuation closure as a hidden parameter. That closure accepts as a parameter the "asynchronous return type" of the function. In other words:

    func foo() async -> Int

Can (give or take error handling and parameter names) be rewritten as:

    func foo(completion: (Int) -> Void) -> Void

This is fairly straightforward; it's just the inverse transformation of what's shown in the conversion section <https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619#conversion-of-imported-objective-c-apis>.

The purpose of `beginAsync` is to start an `async` function while firewalling against the propagation of the `async` keyword to the caller. `beginAsync` does not return a value. By the time `beginAsync` returns, whatever continuation closure was generated for the `body` parameter has been passed off to someone else and some scheduling mechanism has a reference to it and will call it later. That's not giving me too much trouble either.

I'm somewhat bugged by `suspendAsync`. My understanding is that it allows you to take a synchronous function which accepts a callback and turn it into an asynchronous one, where "asynchronous" means "continuation-driven" more that "asynchronously executing", because something like `let foo: Int = await suspendAsync { $0(42) }` looks like it should be legal (although not particularly useful).

The interaction of `suspendAsync` and `await` is that `suspendAsync`, like any other `async` function, accepts a hidden closure parameter, and `await` takes the rest of the function and turns it into a continuation closure to pass as that hidden parameter. The explicit parameters of `suspendAsync` are closures that accept the continuation (either for success or failure), so `suspendAsync` is the primitive that's responsible for translating the rest of an `async` function into something that you can pass as a callback.

That seems to make sense to me (although I might have it wrong), but I'm having trouble with the terminology. I'm not trying to start a bike shed debate here; it's simply not immediately clear to me what "suspendAsync" means for a function that seems more about starting an "old world" asynchronous task than suspending anything (let alone an asynchronous thing).

Flurry of small questions:

* How does this interact with reference counting? The `suspendAsync` functions mark that its closures are @escaping, yet `async` functions don't seem to need to write `self` to access members. What are the dangers?
* Are there ObjC functions right now whose continuation resembles `void(^)(Image* __nullable, Image* __nullable, NSError
* error))`, but for which only one of either the first or second Image parameter has a value if the call succeeded?
* in the DispatchQueue example, is there a meaningful difference between `syncCoroutine` and `asyncCoroutine`? Seems to me that `sync` is currently useful when you need your result before you return, but that doesn't really make sense to me in the context of an async function. Barring deadlocks in the synchronous version, the calling async function should return at the same time in both cases.
* Nit: the DispatchQueue example probably means `self.async`, and probably means `{ continuation() }` (and if not, someone needs to explain me what's going on).

The question where I just can't not get ahead of myself: you wrote that the underlying support can be used to implement generators. This is a feature that is very close to my heart. I understand that the compiler work that turns part of a function into a closure can be reused there; what about the higher-level stuff, do we foresee a set of more or less analogous keywords for generators, or should the async/await terminology be good enough to encompass any type of coroutine-based execution?

Thanks!

Félix

···

Le 17 août 2017 à 15:25, Chris Lattner via swift-evolution <swift-evolution@swift.org> a écrit :

On Aug 17, 2017, at 3:24 PM, Chris Lattner <clattner@nondot.org <mailto:clattner@nondot.org>> wrote:

Hi all,

As Ted mentioned in his email, it is great to finally kick off discussions for what concurrency should look like in Swift. This will surely be an epic multi-year journey, but it is more important to find the right design than to get there fast.

I’ve been advocating for a specific model involving async/await and actors for many years now. Handwaving only goes so far, so some folks asked me to write them down to make the discussion more helpful and concrete. While I hope these ideas help push the discussion on concurrency forward, this isn’t in any way meant to cut off other directions: in fact I hope it helps give proponents of other designs a model to follow: a discussion giving extensive rationale, combined with the long term story arc to show that the features fit together.

Anyway, here is the document, I hope it is useful, and I’d love to hear comments and suggestions for improvement:
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

Oh, also, one relatively short term piece of this model is a proposal for adding an async/await model to Swift (in the form of general coroutine support). Joe Groff and I wrote up a proposal for this, here:
https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619

and I have a PR with the first half of the implementation here:
https://github.com/apple/swift/pull/11501

The piece that is missing is code generation support.

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #20

* I haven't yet read the rest of the thread—this email is already long enough.

The detailed async/await proposal cleared up a few bits for me—in particular, I clearly misunderstood how closely coupled `await` was to the expression with asynchronous components—so please ignore these sections of my email:

## Delayed `await`
## Legacy interop
## Implementation

The "Future directions" section also answers the major question in "Dispatching back to the original queue"—you imagine it being done by the Objective-C importer—although my question about how it would be implemented remains. (I'm also not sure this is really just an Obj-C issue—queue confusion is a Swift problem, too—but that's a somewhat different point.)

···

On Aug 17, 2017, at 11:34 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies