Unpacking Elixir: Concurrency
underjord.io
underjord.io
The thing I like about concurrency in Elixir is that it's there if you need it but it's mostly not mandatory in your code, compared to Javascript where its kind of imposed on you even when it's an hindrance.
I remember one trick I used to do with LiveView on click events etc was to put all async (or "asyncable") code in a spawn function, which would speed up the return.
Not that there isn't any stuff to learn there, you have to understand how actors pass messages, how you can unintentionally bottle neck via call (caller waits for a reply) vs cast (caller keeps going), etc but its very surprise-free because, surprise surprise, Erlang has been built from the ground up for concurrency and generally worked out how it should work 30 years ago instead of bolting some keywords in.
--
[0] By thread I mean the general concept, not specifically OS threads. In this case it includes the fine-grained processes offered by the BEAM VM that underpins Elixir and Erlang.
[1] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
Building on asynchronous sends also makes it conceptually the same to make a local request vs a request on a remote node, although with some additional failure conditions that don't usually make sense to worry about on a single node system.
def process_items(items) do
items
|> Task.async_stream(&Processor.process/1, max_concurrency: 2, timeout: 7000, on_timeout: :kill_task)
|> Enum.to_list()
end
Semantics of regular and concurrent code is the sameOne recent example where I assert that API responses match our OpenAPI specifications here, for the curious:
https://github.com/etalab/transport-site/pull/3351/files#dif...
I'm not saying I'm happy about this or that the meaning has "really" changed; I'm just saying from experience you don't really want to phrase it this way because you only reach those who already know.
source: nodejs dev for 8 years, now fulltime elixir dev for the last 5
Concurrency in Erlang sort of frustrates me...not because it's bad, but because when I use it I start getting pissed at how annoying concurrency is in nearly every other language. So much of distributed systems tooling in 2023 is basically just there to port over Erlang constructs to more mainstream languages.
Obviously you can get a working distributed system by gluing together caches and queues and busses and RPC, but Erlang gives you all that out of the box, and it all works.
The Elixir 'Getting started'[0] guide has you building a concurrent, distributed KV store using nothing but the basics of OTP (effectively the std of BEAM).
0. https://elixir-lang.org/getting-started/introduction.html
Elixir uses the same VM with a different, some including myself would say better syntax. Do you still prefer Erlang syntax?
When I see Erlang syntax, it helps me think in Erlang.
Anyway, I haven’t given Elixir a fair chance, but it’s hard to get past my knee-jerk dislike of all the extra verbiage. I’d rather use LFE if I wanted an alternative syntax with more powerful macros.
There must be a good rationale for that decision. Do you know what it is?
Also in my experience, most of the time, the infrastructure team doesn’t know anything about elixir.
[0] https://dashbit.co/blog/kubernetes-and-the-erlang-vm-orchest...
You could certainly get away without some of the other stuff but, as another comment has mentioned, it requires some infra know-how. Like, you can't "just" use ETS as a Redis replacement without setting it up in a way that its data won't get blown away during a blue-green deploy.
However, in orgs that are primarily Elixir shops, I don't see a lot of Kafka or gRPC. (Redis is just useful, its more than just a queue and K8s and Elixir/Erlang compliment each other, btw.)
And what makes Elixir not need Kafka, Redis or GRPC?
Instead of Redis, you could use ETS for caching. But once you have 2+ instances of your app, you will need to have a centralized caching mechanism, otherwise, each instance will have its own ETS with its own memory, not sharing anything. Unless you decide to use Distributed Erlang and connect all the nodes of your application, which comes with a lot of trouble. Much easier to just use Redis.
And lets say you have multiple teams, each team has its own service(built with Elixir), and you need to have some async communication between those services. What native Elixir solution would you use instead of Kafka?
Same for GRPC. What's the alternative? Connecting all the nodes and using Erlang message-passing?
Just say no to Kafka.
Though I will concede, it's a bit of a bazooka for a mosquito, sort of thing.
ETA: As another couple of comments pointed out, ETS also dies with the node so you've got to handle that also when rolling it.
ETS is cool, but its not a panacea.
that said, having the ability to send messages between machines without having to bring in an external dependency like redis is AWESOME from a maintenance perspective. one less thing to worry about and the system just works. our uptime has been pretty amazing given our small team.
Many companies are using Distributed Erlang but not the way you described: they are using it to exchange messages between nodes running the same version of the software. Imagine that you are building a web application, you can directly exchange live messages between nodes without Redis/RabbitMQ [1]. If you are deploying a machine learning model, you can route requests through multiple nodes and GPUs without additional deps [2]. If you want to see which users are connected to your app, you can exchange this information directly between nodes [3].
In other words, there is a subset of distributed problems that Distributed Erlang solves very well out of the box: homogeneous systems working on ephemeral data. And some of the scenarios above are very common.
If you need to communicate between different systems or you need to persist data, then I 100% agree with you, I would not use Distributed Erlang (at best it would be one part of the solution). I think this distinction gets lost in many conversations and sometimes it leads to false dichotomies: "why Erlang when I have k8s/grpc/redis?" while in practice there is not a lot of overlap. I have written about Erlang/Elixir vs Redis [4] and vs k8s [5] for those interested.
[1]: https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html [2]: https://news.livebook.dev/distributed2-machine-learning-note... [3]: https://hexdocs.pm/phoenix/Phoenix.Presence.html
[4]: https://dashbit.co/blog/you-may-not-need-redis-with-elixir [5]: https://dashbit.co/blog/kubernetes-and-the-erlang-vm-orchest...
Speaking of which, I'm looking forward to using Broadway [1] in a new project here in my company. Here, people are using an enterprise integration engine specialized in the healthcare space [2], with built-in single-branch version control and all actions going through the UI.
As I come from a background of several years with Ruby on Rails, I really hope to convince people to use this great library/framework your company created, since RoR is severely lacking when handling heavy concurrency like when gluing multiple APIs in complex workflows. Software engineers are going to love it, but integration analysts are used to IDEs with GUIs, so we'll need to create a pretty admin dashboard to convince them to switch.
[1] https://elixir-broadway.org/ [2] https://rhapsody.health/solutions/rhapsody/
https://www.youtube.com/watch?v=pQ0CvjAJXz4
(Doh, just realized I was replying to Mr. Elixir himself! And you're familiar with our project anyway :)
I'm not sure I agree at product level: WhatsApp seems to be scaling rather well. I can't say if their use of Erlang is "fully meshed distribution" or not, but it seems to be flying just fine as the world's number 1 messaging platform.
Also, have a look at the Phoenix Framework Channels examples, as it essentially replaces most simple micro-services architectures.
This recipe book covers the common design examples:
"Designing Elixir Systems with OTP: Write Highly Scalable, Self-Healing Software with Layers" (James Edward Gray, II, Bruce A. Tate)
One day, you too may get annoyed with IT complexity, and articles mostly written by LLM chatbots.
Happy computing, =)
for example refactoring something like this:
File.stream!("path/to/some/file")
|> Stream.flat_map(&String.split(&1, " "))
|> Enum.reduce(%{}, fn word, acc ->
Map.update(acc, word, 1, & &1 + 1)
end)
|> Enum.to_list()
to be async, concurrent, (Flow uses GenServers under the hood, A GenServer is a process like any other Elixir process and it can be used to keep state, execute code asynchronously and so on.) File.stream!("path/to/some/file")
|> Flow.from_enumerable()
|> Flow.flat_map(&String.split(&1, " "))
|> Flow.partition()
|> Flow.reduce(fn -> %{} end, fn word, acc ->
Map.update(acc, word, 1, & &1 + 1)
end)
|> Enum.to_list()
The point is the code isn't radically different and is easy to understand if you understand the first block of code.While it's not a close analog, the way Unreal Engine's node-based "no code" Blueprints language approaches asynchronicity just feels so much more natural. Even hopeful pre-hello-world coders seem to conceptually get "well I can't get that because this hasn't happened yet." Having graphical representations of things both in nodes and in-game obviously helps in ways that wouldn't make sense in js, but being built from the ground up to handle it does show that it can be approached more intuitively.
createReadStream("file.txt")
.flatMap((chunk) => chunk.split(" "))
.reduce((acc, word) => {
acc[word] = (acc[word] ?? 0) + 1;
return acc;
}, {}) Any sufficiently complicated distributed program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Erlanghttp://rvirding.blogspot.com/2008/01/virdings-first-rule-of-...
Sadly, I've only ever had one job at a shady startup that used Erlang; I would love to work for a company that sees its power, but sadly it seems that everyone who has not drunk the Erlang koolade thinks that Go is a suitable replacement.
It just annoys me that people see "Go is good at concurrency", and then see "Erlang is good at concurrency", and then assume Go ~= Erlang, which is not the correct conclusion.
The achieving tasks in it or the getting paid to do so part?
Fortunately there’s Agent/OTP, but again, pretty different than other (or popular with beginners - js/python/java) languages.
But comparing:
i = 0
for x in [1, 2, 3, 4]:
i += x
vs i = Enum.reduce([1, 2, 3, 4], fn x, i -> i + x end)
There is nothing inherently "harder" about the functional version, it's just different (and of course comes with its own benefits). Enum.sum([1, 2, 3, 4])
Exist ;) sum([1, 2, 3, 4])
so obviously not my point :P;)
When you have an imperative loop the state of any of the variables youve declared is up in the air. It gets even worse if you've shadowed variable names.
Ultimately I am still as fluent with Ruby as before, but I find developing with Elixir is easier than Ruby (it took a few years to get there though).
Formal attempts have repeatedly failed, even when they exclude message passing, which is the most difficult (and most useful) aspect. If Wadler cannot do it, I'm sure I (or you) cannot.
A typesafe concurrent language is possible, but only a new language layer on top of E/E and the BEAM.
There is the set theoretic types work [1] lead by Jose and a couple of PhDs and also eqwalizer by WhatsApp [2]
[1] https://elixir-lang.org/blog/2022/10/05/my-future-with-elixi... [2] https://github.com/WhatsApp/eqwalizer
https://elixir-lang.org/blog/2023/06/22/type-system-updates-...
https://spacedimp.com/blog/dockerless-setting-up-an-elixir-w...
I am looking for a better representation of concurrency and in my thinking I've found that it feels easier to understand is a timeline grid with rows for independent processeses and columns for time with demarcations for events. You could say a sequence diagram is closest but I'm also thinking of Chrome developer tools with its renderings for rendering, painting, javascript etc.
http://bloom-lang.net/ solves the nonorderly concurrency problems between machines with lattices.
I am unsatisfied how concurrency is represented in the mainstream languages. I find Rust async hard to understand and read at a high level from a schedule perspective. The function colouring issue is painful.
I want an interactive programming environment that lets me create a "tcp-connection" for instance and then register handlers for "on-ready-for-reading" and "on-ready-for-writing" which are system events that are triggered from IO threads that run epoll or liburing. You don't want to block the IO thread loop so you dispatch an event to another thread.
I've been designing a syntax for representing state machines and events that can be fired from different places, it looks like this:
initialstate = state1 | state1a state1b state1c | state2a state2b state2d | state3
It waits for initialstate to be reached, then waits for state1 then it waits for state1a state1b state1c in any order and so on.Ideally we want data flow to be a tree and sprinkled with synchronization points which are barriers where independent tasks synchronizes, where we exchange tasks and data. Shared memory synchronization is great for the amount of data that can be transferred in one-go (you're not writing lots of data into a pipe as in multiprocessing or into a buffer in message passing, message passing can be O(1)) but I don't want to do it on the hot path, Amdahls law.
Another program I've written with Java runtime executes the following program:
thread(s) = state1(yes) | send(message) | receive(message2);
thread(r) = state1(yes) | receive(message) | send(message2);
I've been trying to design a multithreaded server architecture here
https://github.com/samsquire/three-tier-multithreaded-archit...You could implement complicated interlocking workflows with this syntax, because you just need to wait for events as defined.
LMAX Disruptor gets some good requests per second
Effekt or Ocaml 5.0 new events handler system may help get ideas that help you achieve your goal here.
Hmm, am I reading about Elixir or C#... ;-)
Yes, there are other major differences but this entire paragraph can be taken verbatim for C#.
I don't know the .Net runtime well enough to say anything about how it actually executes though.
Vectors are usually also homogeneous about the data they hold, because each element should occupy the same fixed amount of memory, such as i*item_size gievs you back the offset of the element i in memory.
Anyway, in Elixir you can use the Erlang's :array module
Access in O(1) instead of O(nlogn).
Both maps and vectors in Clojure are trees, albeit very shallow trees (32-way branching). The difference lies in the interfaces and the lookup methods. (Maps hash keys and use bits to know which subtree to descend, while vectors use index bits.)
all the JVM languages
all the .Net languages
Rust
Ruby
Python
etc.
irb(main):002:0> ["GFG", "GFG", "GFG", "GFG"].class
=> Array
irb(main):003:0> {1 => "CFG", 2 => "CFG"}.class
=> Hash
irb(main):006:0> {1 => "CFG", 2 => "CFG"}.keys.class
=> Array
irb(main):007:0> {1 => "CFG", 2 => "CFG"}.values
=> ["CFG", "CFG"]
irb(main):008:0> {1 => "CFG", 2 => "CFG"}.values.class
=> Arrayhttps://docs.python.org/3/library/array.html
>>> a = array('B', [1, 2, 3, 4, 5])
>>> str(a.buffer_info()[1] * a.itemsize) + " bytes at address #" + str(a.buffer_info()[0])
'5 bytes at address #4379640336'
>>> b = array('l', [1, 2, 3, 4, 5])
>>> str(b.buffer_info()[1] * b.itemsize) + " bytes at address #" + str(b.buffer_info()[0])
'40 bytes at address #4380812752'All kidding aside, unless I'm interviewing at an Elixir shop, I've learned that Elixir is a little too weird for interviewers who don't know Elixir very well.
With a big caveat that modifying tuples isn't great for performance unless the compiler or optimizer determine that mutating the tuple is acceptable rather than providing a mutated copy.
Even clojure's is a cheat.
This occurred somewhat recently when implementing stable topological sort. I ended up swapping List out for a Vector (Aja is library I used). I received a 100x performance improvement from it.
That's really the only time I've truly hit a List performance roadblock in 7+ years of Elixir.
Nifty, powerful, and simple, like so much of Erlang.
But also like so much of Erlang, I think the modern approaches (several of them) that languages take to serialization is better. It's a good first pass cut at the problem, but I prefer all of the GRPC approach to the problem, the JSON approach to the problem, and honestly just letting the chips fall where they may with most modern serialization libraries. Treat the remote system as not entirely trusted and handle the messages with a bit of skepticism generally works out for quite a bit of scaling. And you get user types back, which means you are no longer stuck on the BeamVM's quite anemic data types.
If you look at the underlying implementations of an Erlang map, you'll see why you're not getting vectors anytime soon.
1> dict:append(a, b, dict:new()).
{dict,1,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
{{[],[[a,b]],[],[],[],[],[],[],[],[],[],[],[],[],[],[]}}}
It's a big pile of linked lists storing assoc lists held together by tuples, with all the component value being dynamically typed. It's nice they didn't cheat on that, but it is... not the most efficient approach to dictionaries, just the one enabled by their type system, such as it is.