
Concurrency in Swift: One possible approach - spearo77
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782
======
lobster_johnson
I'm surprised that async/await is the preferred idiom.

Having worked with async/await in Node.js a lot, it is of course a
significantly better solution than plain promises, but it is also quite
invasive; in my experience, _most_ async code is invoked with "await". It's
rare to actually need to handle it as a promise; the two main use cases where
you want to handle the promise as a promise is either when doing something
like a parallel map, or when you need to deal with old callback-style code
where an explicit promise needs to be created because the resolve/reject
functions must be invoked as a result of an event or callback.

Would it not be better to invert this -- which is the route Erlang and Go went
-- and make it explicit when you're spawning something async where you don't
want to deal with the result right away? In Go, you just use the "go" keyword
to make something async. So the caller decides what's async, not the callee.
If callers arbitrarily decide whether to be async or not, a single async call
ends up infecting the whole call chain (which need to be marked "async" unless
you're explicitly handling the promise/continuation without "await").

~~~
wahern
Both Erlang's and Go's concurrency model is thread-based. An "async" operation
invokes a function on a separate call stack. The function doesn't need to know
which call stack it's invoked on. The functions that that function calls don't
need to know, either. Lua's coroutines are similar.

The problem with the above approach, however, is that you can't create
hundreds of thousands of threads while also transparently supporting the
traditional C ABI. C ABIs aren't designed to dynamically grow the stack, and
so any thread that needs to invoke C (or Objective-C) code must always create
threads with very large stacks (on the order of hundreds of several hundred KB
or even megabytes) if they want to support legacy code.[1]

Languages that don't want to put the effort into growable stacks have no
choice but to implement a solution that requires annotating the function
definition, directly or indirectly.[2] The annotation tells the compiler to
generate code that stores invocation state (temporaries, etc) on a dynamically
allocated call frame (usually allocated by the caller) rather than pushing
them onto the shared thread stack. This is true whether or not the function is
a coroutine that can yield multiple values before finishing. Basically,
without using a thread-based model, you can never put the caller in full
control.

[1] Work on GCC Go necessitated adding a feature to GCC called split stacks.
So GCC can actually compile C and (I think) C++ code that can dynamically
extend their stacks. However, for it to work properly you have to compile
everything with split stacks, including libc and all dependent libraries.

[2] Technically a compiler could emit two versions of a function, one that
uses the thread stack for temporaries, and one that uses a dynamically
allocated frame. That would put the caller in control. Some languages with
very complex meta-programming capabilities (like various Lisps) can do this by
making the await keyword a function which literally re-writes the callee into
an async function that stores temporaries on a caller-provided buffer. So you
can implement it without any compiler support. But it's still limited because
the functions invoked by the async function would have to be rewritten
recursively. The thread-based design is really the best approach, but it's a
non-starter for many languages because of concerns about interoperability and
legacy support. JavaScript rejected a thread-based model because existing
implementations were too heavily dependent on the semantics of the traditional
C stack, and they didn't want to throw away their existing investments.

~~~
kllrnohj
> The problem with the above approach, however, is that you can't create
> hundreds of thousands of threads while also transparently supporting the
> traditional C ABI. C ABIs aren't designed to dynamically grow the stack, and
> so any thread that needs to invoke C (or Objective-C) code must always
> create threads with very large stacks (on the order of hundreds of several
> hundred KB or even megabytes) if they want to support legacy code.

Where did this claim originate from? It gets tossed around all the time in
greenthread discussions but it's completely false. When you create a real
thread even though it has an 8MB stack or whatever it doesn't actually
allocate 8MB. 8MB is not the allocation size, it's the growth limit. The stack
grows dynamically allocating memory when necessary until it hits that limit.
The C/C++/<insert any language here> ABI doesn't need to be compiled for this
because it's just page faulting. Standard OS behavior for decades.

~~~
chrisseaton
So it still consumes 8 MB of address space though? On a 32 bit system that
would severely limit the number of threads you could allocate in each process
wouldn't it? How do you allocate 8 MB of stack times a hundred thousand, in a
32 bit address space?

~~~
RX14
Yes, it breaks on 32bit but that's an acceptable tradeoff in most places
because the vast majority of places where a modern language like swift would
actually be used are 64bit.

~~~
chrisseaton
The person I was replying to asked where the 'myth' came from. It came from 32
bit systems, where it isn't a myth. Even if it doesn't apply today (and
administrating thousands of pages per thread still isn't free even if you have
the address space, so I'm not sure it doesn't still apply really), that's
where the 'myth' came from - that's the answer to their question.

------
liuliu
Async / await also need a idiom to accompany on how to support cancellation.
In practice, cancellation happens a lot because the unbounded latency for
async operations. Without a throughout support, async / await syntax is
probably usable on server side somewhat but still hardly applicable on client
side (as the latency is unbounded). On the other hand, C# does go through the
pain and added cancellation support to all its standard libraries async API.

~~~
jakobegger
I can't upvote this enough. In practice, supporting cancellation is one of the
most important and tedious parts of asynchronous programming.

It's easy to start a some task with a completion handler. Even error handling
isn't that hard, since communication of a failure goes in the same direction
as communicating success.

But cancellation goes into the other direction! So whenever you start an async
operation, you need to somehow store a handle or something, so you can cancel
it later on.

The actor model makes this even worse -- how do you cancel a command that you
previously sent? How do you tell an actor that they don't need to perform an
operation, if the operation is still on the queue, or that they should abort
the operation if they are already working on it?

If the actor model doesn't have an answer to this problem, developers won't be
able to use it as is, and they will have to build additional abstractions on
top of it before they can use it.

~~~
zzzcpan
You create an actor that performs an operation and simply kill it if you want
to cancel that operation. Promises/futures with cancellable contexts are
equivalent to actors.

~~~
kaeluka
To implement killing is not easy.

Edit: to implement it in a well-performing way.

~~~
tonyg
This is not true: Erlang is a counter-example.

~~~
kaeluka
Is the implementation in Erlang remarkably simple? How does it work?

------
jzelinskie
As someone who doesn't develop for the Apple ecosystem, I can't quite find one
place that articulates well what the Swift development philosophy is and what
it brings to the table besides just being modern language with shims for
interacting with legacy Apple APIs. Why would I use Swift on Linux?

Most of the posts that I see related to Swift are RFCs evaluating solutions to
problems in other languages. I rarely get to see the actual solutions being
integrated into the language. Is this just a result of HN readers caring more
about language design than learning to leverage the Swift language?

~~~
tommymachine
I just wrote a swift api that I'm running on Ubuntu 16.04. I wasn't hesitant
to use swift because first, it substantially outperforms node on many
benchmarks [1] [2] and second, the community of people who are excited about
swift seem to have made up for its young age. You can find workable tutorials
and help online for server side Swift. And there are multiple backend
frameworks to choose from. Having type safety on the server is as great as it
is on the client. It's also great to be able to write the server in the same
language as the client. Makes for a VERY smooth dev process. Why wouldn't you
use swift on the server? (my guess is fear of new tech -- which can be healthy
many times but, IMHO, at least in the case of server side swift, it may be a
bit irrational!)

1\.
[https://benchmarksgame.alioth.debian.org/u64q/compare.php?la...](https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=swift&lang2=node)
2\. [https://medium.com/@rymcol/linux-ubuntu-benchmarks-for-
serve...](https://medium.com/@rymcol/linux-ubuntu-benchmarks-for-server-side-
swift-vs-node-js-db52b9f8270b)

~~~
throwaway91111
How does ARC hold up for long-lived servers? Are the leaks manageable?

~~~
coldtea
What leaks? You only get leaks if you have cycles that you forgot or don't
close non-memory resources that you keep referencing.

Which is not that different than with a GC.

~~~
kartickv
No, GCs collect reference cycles. Whereas a (strong) reference cycle in ARC in
an operation repeated many times in a long-running server or something adds
up.

Worse, sometimes, you don't even know if you're creating a leak. For example,
I recently had to call, given two gesture recognisers a and b:

a.requireToFail(b)

A is a long-lived object. B goes away when the current view controller is
popped. But not if A keeps a strong reference to it. Does it? Probably no one
without access to the source code of UIGestureRecognizer knows!

------
haimez
Feels like the elephant in the room for the description is not once mentioning
Futures but instead jumping straight to using async / await keywords and the
actor model.

As evidenced by C#, you can't avoid leaking the type signature of async
operations if you actually support generic programming- so while that's a nice
ergonomics improvement, it only adds complexity to the actual concurrency
model. Go enthusiasts out there will appreciate that go solves this by
refusing to support PROGRAMMABLE generic abstractions at all (looking at you,
channels and map).

Referencing the actor model and making it first class is interesting, but
probably a mistake. Actors are hard to reason about because they're so
flexible. Pony is a good recent attempt at combining static types with actors,
bit they didn't put performance into the "non-goals" section of their language
spec.

If you want task level concurrency and you want it to play nice with your type
system, you have to start with Scala and work backward to the alternative
implementation choices you're going to make because it checks all the boxes of
all the "goals" and ALSO has a very mature actor model implementation that
doesn't require promoting actors to keyword status in the language.

~~~
andrewbarba
He heavily discusses Futures, and a sudo-implementation, in the full proposal
here:
[https://gist.github.com/lattner/429b9070918248274f25b714dcfc...](https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619#entering-
and-leaving-async-code)

~~~
haimez
Forgive my naivety, but parent article seems to contradict the github post
linked in your reply in quite a few ways. What is the time / design iteration
relationship between the two, and why (not to put you on the spot)? These are
VERY different proposals.

~~~
ash_gti
One spec is about Async/Await and the other is a high level design approach
for the language (which does include a reference to the async/await proposal,
its under the section titled `Part 1: Async/await`).

------
spearo77
And the supporting swift Pull Request is
[https://github.com/apple/swift/pull/11501](https://github.com/apple/swift/pull/11501)

------
zzzcpan
Maybe it's time for Swift to decide whether it's going to stick to multicore
systems or actually leave this single box mindset and enable distributed
systems programming. I think designing for distributed systems first could
lead to a lot of right choices from the beginning and would still allow for
various optimizations later to get the best performance from multicore.
Alternative is not very promising and provides a lot of room for really stupid
mistakes. Either way, I'm glad to see actor model getting more traction.

~~~
throwaway91111
Well, multicore isn't going away; concurrency (if not parallelism) is
necessary for responsive UIs.

Did you have any specific improvements? The big requirements for distributed
programming are mostly protobuf serialization and tcp/http; swift has both
already.

~~~
zzzcpan
I was thinking more in terms of first class support for distributed actors,
with global addressing, message routing, etc. Not designing for multicore
performance first, the way Pony did.

------
panic
Definitely some cool ideas here! The motivation seems a little high-level to
me, though. Language features ought to help you solve concrete, real-world
problems. I can understand what problem async/await solves -- there's a great
example with code -- but the actor stuff isn't as clear-cut.

------
lngnmn
Software interrupts is the right answer to concurrency nonsensical hype.
Sometimes old is gold.

Wrapping specialized interrupt handlers into some higher lebel API, such as
AIO, is the right way.

Async/await is a mess. Concurrency cannot be generalized to cover all the
possible cases. It must have specialization.

Engineers of old times who created the early classic OSes were bright people,
contrary to current hipsters.

Once popularized, flawed abstractions and apis such as pthreads would stick
(only idiots would accept shared stack and signals).

Look at how an OS implements concurrency and wrap it into higher level API.
That's it.

~~~
dullgiulio
Your comment is rude and factually very wrong. The new languages we discuss
are definitely not written by "current hipsters", whatever you meant with
that.

How does the OS implement concurrency, which is a userspace problem?

Software interrupts alone are not enough, you still need some language support
to it. I am not sure you really know what concurrency even means...

~~~
lngnmn
> concurrency, which is a userspace problem

This is another gross misconception, like an attention to manage resources
from the userspace, the way JVMs or Node are trying to do by poorly
reimplementing OS subsystems.

Concurrency primitives _must_ be implemented by an OS - it will eliminate all
the userspace problems - no busy-waiting, no polling, no waking up for every
descriptor, etc.

Ignoring an underlying OS instead of having thin wrappers, like Plan9 from
userspace does, is exactly what is called ignorance (lack of awareness of a
better way).

------
weitzj
Related Tweet from Joe Armstrong:
[https://twitter.com/joeerl/status/898444575668404224](https://twitter.com/joeerl/status/898444575668404224)

------
hellofunk
> the speed of light and wire delay become an inherently limiting factor for
> very large shared memory systems

Wow, cool to see actual speed of light mentioned as a factor, never seen that
before.

~~~
tambourine_man
Grace Hopper - Nanoseconds

[https://youtube.com/watch?v=JEpsKnWZrJ8](https://youtube.com/watch?v=JEpsKnWZrJ8)

------
jorblumesea
Doesn't Chris work at Google now? Where does he find the time to just go off
and implement the Actor Model? I assume this was done when he was still at
Apple?

~~~
valuearb
Swift is open source. He's making a proposal.

He also seems to carry a little weight in the community. I'm guessing his
proposal might get implemented.

~~~
jorblumesea
Yeah but if you make a proposal with a pull request it has to work, at least
in theory. I don't think I've ever just "implemented the actor model" in a
weekend haha. Props. I guess when you wrote the language...

------
tambourine_man
Must be crazy for Apple to have such an influencing voice on Swift and LLVM
working for its archenemy

~~~
valuearb
Google's paying Apple billions per year and has one of it's senior software
architects helping improve their core development language.

I'd say crazy like a fox!

~~~
MBCook
Not only that, Apple is apparently responsible for 50% of the mobile ad
revenue Google pulls in.

------
perfectstorm
correct me if I'm wrong but this is what C# has? I recall writing async and
await when I did a Windows Phone app many years ago.

~~~
aaronbrethorst
Yes, exactly. FTA:

    
    
        There is a well-known solution to this problem,
        called async/await. It is a popular programming
        style that was first introduced in C#

------
0xbear
Any such model MUST show what error handling will look like. Otherwise you end
up with "log to console and ignore" approach so prevalent in Android SDK
examples, which people _will_ copy unchanged.

~~~
RubenSandwich
Considering that Mr. Lattner pays tribute to the Akka library which is based
on Erlang style actors. I'm going to assume he is going to go with Erlang
style error handling. Allow actors to throw and allow supervising actors to
catch those throws.

~~~
haimez
"Erlang style" supervision error handling is short-hand for opt-in, optional
error handling without any compiler support. That's a pretty serious trade-off
to not address directly.

~~~
colefichter
What you describe sounds like a consequence of Erlang's dynamic type system,
rather than its concurrency model. Besides, don't most languages have "opt-in"
error handling? For example, I work mostly in C# and if you don't catch an
exception, your program will crash. How is that different? At least in Erlang,
my program might restart itself. Sadly that doesn't happen on most other
platforms.

