Let’s Talk Concurrency: Panel with Sir Tony Hoare, Joe Armstrong, Carl Hewitt
erlang-solutions.com
erlang-solutions.com
I disagreed with Tony Hoare about using synchronous communication as the primitive because it is too slow for both IoT and many-core chips. Instead, the primitive for communication should be asynchronous sending and receiving, from which more complex protocols can be constructed.
Also, I disagreed with Tony about sequential actions (using ";") as being foundational. Instead, concurrent actions are foundational for digital systems as follows:
* Receipt of a communication activated sending other communications
* An Actor received one communication before it received another communication
Consequently, a computation is a partial order of causality. Tony and I did agree that tooling is needed for navigating the partial order. We just disagreed about whether sequential actions (using ";") are foundational.Furthermore, class hierarchies are not a suitable foundation for Scalable Intelligent Systems. Interfaces instead of subclassing should be used for IoT communication. Also, entities and descriptions in large ontologies do not fit in an object class hierarchy, e.g., Java and C++. Subclassing is not secure because it allows a subclass to impersonate a superclass.
I disagreed with Joe Armstrong about requiring use of external mailboxes because they are inefficient in both space and time. Instead of requiring an external mailbox for each Actor, buffering/reordering/scheduling should be performed inside an Actor as required.
Of course, Tony and Joe made other great points with which we agree entirely. See the following for more information:
http://web.stanford.edu/class/ee380/Abstracts/190123.htmlHowever these days we see people build actor frameworks using CSP (golang) and "restricted shared memory" in Rust http://actix.rs
Isn't it ironic? Yet shows that the actor model can be inclusive of many "competing" paradigms, and gain from them, perhaps even need them to scale vertically?
Citadels are larger scale Actors which can be incorporated into other Citadels, where a Citadel is an Actor for a systems of Actors (perhaps including IoT).
See the following:
http://web.stanford.edu/class/ee380/Abstracts/190123.htmlFor example, I tried to implement a Akka bittorrent client and found it very difficult and confusing. You end up with a big graph of actors in which most of the messages sent are related to keeping track of state and actors.
Moreover, actors are untyped so it's not obvious what messages they accept of send. Without a lot of documentation and discipline, the code becomes extremely unreadable very very quickly. Also bugs are very hard to find and very easy to create.
Maybe it's less elegant, but for a lot of problems, just using select or epoll is a lot easier.
- The tendency is to start extremely elaborate and become much more minimalistic with experience.
- The educational materials do too good a job at explaining advanced techniques without warning about how important it is to keep things simple in practice.
- A complex actor system or inheritance hierarchy should both reflect AND help tame complexity in the problem you're solving. If it doesn't meet both of these criteria, it should be simplified.
- There is no semantic reason that any relationship in a problem domain needs to be modeled by inheritance or actors despite being painful to program that way. The technique should only be applied where it helps.
I think (not from personal experience, but from what I read) that the big exception to the above is when you're using actors to optimize latency or throughput on a big multi-core machine. Then you will find yourself taking on complexity for the sake of maximum performance. I think educational materials often present techniques from these difficult, carefully-built systems as if they're helpful for writing simple, small services. This is also a parallel to inheritance, if you take a step back: high-performance data processing systems are the canonical problem for this era, just like complex native GUI programming was the canonical problem in the heyday of big inheritance hierarchies. Lessons from those domains can be misleading when applied to simpler problems.
We had to control various devices based on sensor data, but we also needed to make sure that the workload was distributed across the devices in an efficient way. To implement that, we had one actor per device that was solely responsible for adjusting the device output based on sensor data. Then we had another actor that was responsible for looking at the behavior of a cluster of devices and distributing the load between them. This "load distribution" actor wouldn't control the devices directly, but instead would send adjustments to the actors directly controlling the device.
I find it very natural to anthropomorphize actors. That is, if you were to think about how you would organize a group of people to do the work of the system (in a very Taylorist sort of way) the roles of those people map neatly onto the actors one would create.
One common pattern is the manager/supervisor/worker pattern[0]. I see this as mapping fairly neatly onto the project manager/people manager/employee structure that you see in many organizations.
Like, show me the producer-consumer pattern using actors.
https://github.com/foobatman/producer-consumer-akka-actor/tr...
There are many more examples out there. Anything that a multi-threaded program can do, an actor can do..
I just think that actors are inherently more complex than threads for simple problems. For complex problems, I think actor are probably a better choice.
A good example of a simple program is managing multiple TCP connections. At least in my (somewhat limited) experience with actors, polling TCP file descriptors is easier to manage creating an actor for each one.
Really, it's not obvious? Of course you can: instead of shared memory you now have each actor having its own memory, instead of using locks and working on shared memory you now have messages and can ask actors to do something with their memory and send you the result back.
You can implement locks using actors if you really want to. An actor represents the state of the lock and stores which actor locked it and only responds to the unlock message from that actor and ignores all lock and unlock messages from all the other actors until it is unlocked.
Please see the YouTube video here:
http://web.stanford.edu/class/ee380/Abstracts/190123.htmlSo in order to understand our systems with huge graphs of actors, we should attempt to develop something akin to the heat equation for actors. Trying to understand the behaviour of every individual actor in the context of the whole system is just too much.
For example, if anAccount:Account then the following
anAccount.deposit[$5]
is defined as follows: Account.send[anAccount. deposit[$5]But the real power comes from the BEAM. Turns out modern servers map very strongly to phone switches of the past, and the distributed system primitives given by the BEAM keep on ticking, 30 years later. Modeling a web server as a single process per request, the supervision model, and the power of preemptive scheduling is something I don't see in other languages, at least as explicitly. Preemptive scheduling is really a wonderful thing, and I don't think Go or Rust provide this. Please correct me if I'm wrong. This is to say nothing of the observability, hot code reloads, or any of the more fundamental parts of the BEAM that you wind up needing in practice.
I'll be frank, I think Go is an unnecessarily verbose language. I don't like reading it, and any time I've had to write it, I have not enjoyed it. I find Go's concurrency model worse than Erlang's despite being similar at first glance. GenServers are a much better abstraction to me than GoRoutines and friends. If it weren't from Rob Pike and the marketing of Google, I don't think it would be nearly as popular as it is. The type system from Rust is great, and the borrow checker is a fantastic addition to type systems especially in that class of language, I have no use for Rust in my daily life. It is on my short list of languages to become more familiar with, though.
That's how most production websites of the past 20 years have been built, but these services are pushed up to the OS level rather than the language level. Apache, PHP, CGI, and everything built on that ecosystem used a process-per-request model. The OS provided preemptive scheduling. If you were doing anything in production you'd use a tool like supervisord or monit to automatically monitor the health & liveness of your server process and restart it if it crashes. The OS process model restricts most crashes to just the one request, anyway.
There was a time in the early-mid 2000s when this model gave way to event-driven (epoll, libevent, etc.) servers and more complicated threading models like SEDA, but the need for much of that disappeared with NPTL and the O(1) scheduler for Linux, though process-creation overhead still discourages some people from using this model. Many Java servers are quite happy using a thread-per-request or thread-pool model with no shared state between threads, though, which is semantically identical but with better efficiency and weaker security/reliability guarantees.
Now, there continues to be a big debate over whether the OS or the programming language is the proper place for concurrency & isolation. That's not going to be resolved anytime soon, and I've flipped back and forth on it a few times. The OS can generate better security & robustness guarantees because you know that different processes do not share memory; the language can often be more efficient because it operates at a finer granularity than the page and has more knowledge about the invariants in the program itself. One of the interesting things about BEAM (and to a lesser extent, the JVM) is that it duplicates a lot of services that are traditionally provided by the OS or independent programs running within the OS. In some ways this is a good thing (batteries included!), but in other ways it can be frustratingly limited.
Both approaches _do_ give me a very straightforward programming environment for isolated processes, although the isolation guarantees are smaller in Erlang. I'd like to think it's easier to break the isolation for cross process communication with Erlang, but that's probably debatable.
In my mind, the Erlang model is validated by the Apache model, but it adds scale in a way that doesn't require a mental flip to event-driven programming (although, beam itself is certainly handling IO through event loops with kqueue or epoll or what have you underneath).
It's somewhat less infeasible now than it was in the early 2000s. The main barriers to C1M with an OS process per connection are:
1. Stack size. With 8M stacks 1M processes would take up 8 TB of RAM.
2. Process creation overhead - loading the executable into memory, setting up global context, opening sockets.
3. Context-switching overhead: swapping page tables, TLB flushes, saving registers, etc.
For #1, recent versions of Linux will happily let you create threads or processes with 4K stacks now. They also don't actually allocate the memory for the whole process, they just map pages, and then the page fault is what assigns a physical page to a virtual address, so if you never touch a memory location it doesn't exist in RAM. For #2, new processes get COWed from their parent and can inherit file descriptors as well, so all the read-only data (executables, static data, MMapped files, etc.) is essentially free. #3 is a legitimate reason why language-based solutions are faster (they don't have to flush the whole TLB on context-switch, and know exactly which registers they're using), but mostly affects speed rather than concurrent connections.
Very niche use case and even more in the context of serving HTTP requests, where the JVM/Go/C#/Rust and even nodejs will smoke erlang because it can't compete in raw performance.
This is only true in part. The fact is that Go is a supremely accessible language (warts and all) that helps you get things done.
Had Go been created while their authors would be working elsewhere, and it wouldn't have taken off like that.
Naturally one can use Dart as counter example, but it only failed because Chrome and Angular teams weren't willing to keep pushing it forward.
Fucshia is currently also exploring other UI stacks as per available commits.
Currently it looks like internal teams competition.
Sure, it would not get the initial visibility. But we would not keep using it if was an ineffective language.
For example, even though I am not a big fan of Go, I have to use it when customers required us to deal with Docker or K8S.
Just like I have my issues with C, but would certainly use it when writing an UNIX driver.
Programming languages are products, and get used because of the eco-systems they carry along, bullet point features are usually secondary to that.
> Programming languages are products, and get used because of the eco-systems they carry along, bullet point features are usually secondary to that.
Non-sequitur.
https://fosdem.org/2019/schedule/event/kubernetesclusterfuck...
https://www.welton.it/articles/programming_language_economic...
And it absolutely does make sense to view them as products in order to understand their uptake, or why they don't become popular.
I maintain that it is a non-sequitur, if not patronizing, to state the obvious facts about software language eco-systems. My perception remains that Go sufficiently delighted a critical mass of developers who then proceeded to create the said eco-system. Mere marketing can not engender a vibrant community.
> a critical mass of developers
How do you reach a critical mass of developers without something that looks like "marketing"?
You mention Kubernetes, but forgot all the other widely used projects that are not from Google: Docker, Grafana, etcd, all the Hashicorp tools ( terraform, packer, consul .. ), Prometheus, InfluxDB, Hugo, CockroachDB ect ...
Two others things:
- deploying Erlang / Elixir app is difficult ( even with distillery... )
- Erlang is slow, much much slower than Go
Most applications at least have connection state, at the least a TCP connection. It is at minimum disruptive to disconnect a million clients and have them reconnect. Certainly, your service environment needs to be able to handle this anyway [1] in case of node failure, but if you do a rolling restart of frontends, many active clients will have to reconnect multiple times which adds load to your servers as well as your clients. Actually disconnecting users cleanly takes time too, so a full restart deploy will take a lot longer than a hot code reload, unless you have enough spare capacity to deploy a full new system, and move users in one step, and then kill the old system.
Certainly, hot loading can introduce more failure modes, but most of those are things you already need to think about in a distributed system -- just not usually within a single node; ex: what happens if a call from the old version hits the new version.
[1] There are some techniques to provide TCP handling, but I'd be surprised to hear if anyone is using them at a large scale.
Hot code reload is imo a bad practice and should be avoided.
https://kubernetes.io/docs/tutorials/kubernetes-basics/updat...
That's the power of Kubernetes, and since it's very popular the community and tooling are great, good luck replicating that with BEAM.
Also, message-passing and the actor model is not a particular design focus of k8s compared to BEAM.
The "a lot" part is emphasized. It has to be something massive? Classes? Objects?
With the same token one can say most other languages lack a lot as well. That "lot" would be fault tolerance. Notice how most operating systems today use isolated processes instead of sharing memory like Windows 3 or DOS did. There is a reason for that. When the word processing application crashed, it would take down the calculator and media player with it. So modern operating systems have isolated concurrency units. And so do languages built on BEAM VM.
And of course you could still spawn OS processes and run a language that uses shared memory between its concurrency units (threads, goroutines, callback chains). But you can't spawn too many. Or even worse, everything has to run in a container, so now you have a container framework as another layer. And the question then becomes "how many layers of abstraction can you add to an ecosystem before it is unusable?" ;-)
> Elixir is trying to fill those gaps, but how many layers of abstraction
Elixir is not built on top of Erlang the language. It compiles to the same BEAM VM bytecode. But the intermediary representation layers between the running bytecode and the language syntax was already there. They didn't add another layer on "top" so to speak.
> They embraced actor model,
Don't think so. The creators at the time had no idea about the actor model. They embraced fault tolerance, concurrency and low response time most of all.
Rust has full memory safety for concurrent code (if you restrict yourself to the Safe Rust subset), unlike other commonly-used languages such as C/C++, Go, JVM/CLR-hosted languages etc. This provides a far more general model of concurrency than something like the Actor model; it can also express other patterns very naturally, such as immutability (like Haskell), channels/CSP (like Go), plain old synchronized access etc. Of course Actors can be expressed too, as can isolated processes ala Erlang (that's what exclusive mutability is for!) but you're never restricted by the model.
But I also think Rust has a steeper learning curve and is too low level for many cases. It wants the user to think well and hard about lifetimes, memory layout, ownership, whether to use threads (can you spawn 1M threads easily?), if a thread crashes can you supervise it and restart it, or maybe use futures (promises?) instead. Those are useful thing to think about and might make sense when writing a storage layer, a fast proxy, or a network driver, but that's too many low level choices to think about when say I want to add an item to a shopping cart.
It doesn't. Actor model is as general as it gets for concurrency. Any constraint only makes it less general.
Actually it is, at a semantic level. Elixir source is reduced to Erlang's abstract syntax tree format, which is represented in Erlang terms. The same Erlang compiler is used to generate BEAM code for both languages. This isn't just a detail - the ramifications of using Erlang semantics permeates the language. But not to the language's detriment in any way.
I do like how the Actor Model and Erlang work arrived at the same concepts independently. It seems like an extra validation of the concept.
Until that happens, BEAM is unfortunately a hard dependency on getting the concurrency model fully "right".
Like?
> With go and rust available,
Yeah, tell me when those “modern languages” will be distributed, stop using stop-world GC, and contain at least 1% of introspection Erlang has.
The evidence says that modern people mostly don't care about distribution, don't care about stop-the-world GC in a very large percent of the cases (especially as stop-the-world time is getting shorter and shorter), and don't need Erlang-style introspection very often. I know how useful the latter in particular is, because I also used Erlang for a long time and I used it a lot. But again, what happens, especially when you''ve got the only solution, is that the one solution selected early gets worked into the system very deeply and looks very fundamental, but that doesn't mean that it's the only viable way to do it. So when running an Erlang system, I needed a lot of introspection to keep it running. But when I run non-Erlang systems, I do not simply collapse into a heap and wail that I don't have introspection and watch helplessly as the system collapses around me... I solve the problem in other ways. Entire communities of people are also solving the problem, and sharing their solutions, and refining them as they go as well, just as Erlang did with their solutions.
The Erlang community has basically been complaining for the whole nearly 10 years of Go's 1.0+ existence that it doesn't have every single Erlang feature, but it was never about having every single feature that Erlang has. Erlang is a local optimum, and while I think it's a very interesting and educational one (and I mean that, quite sincerely and with great respect; anyone designing a new language today ought to at least look at Erlang and anyone designing a language with a concurrency story ought to study it for sure), I'm not even remotely convinced it's a global optimum. To get to any other optimum does mean that you'll have to take some steps back down the hill, but if all you look at are the steps down the Erlang slope but not the steps up some other slope, you won't get the whole story.
(I would call out the type system in particular as something deeply sub-optimal. I understand where it came from. I understand why someone in the 1986, with 33 fewer collective years of experience than we have now, would say that the easiest way to have total isolation is to use total immutability, and the simplest way to ensure types can't go out of sync is to not have types. But it crunked up the language to have immutability within a process (that is, "A = 1, A = 2" could have totally worked like any other language without breaking Erlang itself; have separate operators for what = does today and an operator for "unconditional match&bind" and everything works fine), when all it needed was to ensure that messages can't contain references/pointers but can only copy concrete values. And it doesn't solve the problem of preventing things from going out of sync to simply not have types, because you still have types in all of the bad ways (if two nodes have two different implementations of {dict, ...}, you still lose in all the same ways), you just don't get types in any of the good ways. It was a superb, excellent stab at the problem in 1986, and again, I mean that with great respect for the achievement. But in the cold harsh world of engineering reality, it is one of the huge openings for later languages to exploit, and they are. There are others, too; this is in my opinion one of the biggest, but not the only.)
Syntax-wise Elixir already allows you to reassign a variable so the "A = 1, A = 2" example you mentioned is moot for the developer productivity point of view (though I understand that it's still valid from an efficiency standpoint, since Elixir actually just creates new variables with different suffixes under the hood).
Stop-the-world GC is a pain to deal with in tons of domains. Games, communications, control, various system-level tools. Just because a lot of web developers don't care about those doesn't mean the domains are small.
I've stated previously I'm an Erlang fan, for several of the reasons you've highlighted. I similarly don't believe it's a "global maxima".
Perhaps the most saddening observation is the number of languages that have come after Erlang - intended for server-type loads - that haven't learned from and built on its strengths in concurrency and fault tolerance.
I remember a separate discussion between Joe Armstrong and Alan Kay[0] where Kay posed the wonderful question (paraphrasing): "what comes next that builds on Erlang?"
That's a tempting prospect. My personal wish list would include 1st class dataflow semantics and a contemporary static type system and compiler that's as practically useful as Elm's today.
The key point is to build on what's been proven to work well, not throw it away and have to re-learn all the mistakes from scratch again.
Go's stop-the-world pauses are usually sub-millisecond for heaps measured in the tens or hundreds of gigabytes.
See the following:
Are you saying that the Erlang VM enforces mutual exclusion within its Actors, and that mutual exclusion should not be enforced within Actors?
However, the region of mutual exclusion can have holes so that
* activities can be suspended and later resumed
* other activities can use the region of mutual
exclusion while a message is being processed by
another Actor
For example, a readers/writer scheduler for a database must be processing multiple activities concurrently, which is very difficult to implement in Erlang.Could you elaborate on that? What exactly does erlang as a language (tooling/libraries/frameworks aside) lack in your opinion? What do those modern language have that erlang doesn't?
wasn't that the original goal for erlang?
As a web developer, I've been quite happy learning Elixir/Phoenix in the past year, and learning Erlang has been on my list, so I'm very interested in hearing from people with more Erlang experience when it comes to 'web stuff'.
I don't do much "web stuff" with Erlang. The closest I go is probably HTTP-based control interfaces for other services (routing, validate input, do something in the system and return a response).
I usually turn to Python and Django when I want to create web stuff. Generic views, the forms API, DRF, admin, and an ORM that integrates well with all of the aforementioned are godsent if you just want to get something online quickly.
I still have old Django project that I wrote for a customer back in 2007. Besides upgrading Django two times a year and updating the ui a little it has been running w/o problems ever since. And I still find my way around the code instantly.
I have yet to try something that comes close to being as convenient to work with.
Formex library is outdated and doesn't seem to work on Phoenix 1.4 but it makes it so much better to create form.
With Phoenix form library you have to add code in several files (schema, context, controller, view, and template). I can never remember all of it correctly so I have to refer to pragprog text book all the time. Formex require less.
I don't believe this is some sort of a deal breaker but it is important enough. There are tons of web app require forms CRUD operations admin like stuff to insert data via web.
Naturally since Ada is tainted by its history, other approaches more interesting to the common crowd are needed.
Isn't this even more true when speaking about serverless and all the surrounding technologies?
Simplifying, modernizing, and exposing all of the tracing and debugging and releasing and other goodies inside of BEAM would be huge.
A good point but most would think of the two the same. I know I had a hard time getting some ideas across on some research I was doing until I separated these two concepts out. Then people started to understand.
As in they're independent of each other?
The answer would be no. Concurrency doesn't require parallelism but parallelism require concurrency.
The second answer to this quora link explains it way better than I can. https://www.quora.com/What-is-the-difference-between-concurr...
I don't think it does. For example in a SIMD system there is parallelism, but no concurrency. Operations are being performed in parallel, but there is only a single program (SI..) and no concurrent tasks.
http://composition.al/blog/2014/11/24/yet-another-blog-post-...