Comment by asa400

4 days ago

Sorry, this is going to be a slightly longer reply since this is a really interesting question to ask!

Elixir (and anything that runs on the BEAM) takes an entirely different perspective on concurrency than almost everything else out there. It still has concurrency gotchas, but at worst they result in logic bugs, not violations of the memory model.

Stuff like:

  - forgetting to update a state return value in a genserver
  - reusing an old conn value and/or not using the latest conn value in Plug/Phoenix
  - in ETS, making the assumption nothing else writes to your key after doing a read (I wrote a library to do this safely with compare-and-swap: https://github.com/ckampfe/cas)
  - same as the ETS example, but in a process: but doing a write after doing a read and assuming nothing else has altered the process state in the interim
  - leaking processes (and things like sockets/ports), either by not supervising them, monitoring them, or forgetting to shut them down, etc. This can lead to things like OOMs, etc.
  - deadlocking processes by getting them into a state where they each expect a reply from the other process (OTP timeouts fix this, try to always use OTP)
  - logical race conditions in a genserver init callback, where the process performs some action in the init that cannot complete until the init has returned, but the init has not returned yet, so you end up with a race or an invalid state
  - your classic resource exhaustion issues, where you have a ton of processes attempting to use some resource and that resource not being designed to be accessed by 1,000,000 things concurrently
  - OOMing the VM by overfilling the mailbox of a process that can't process messages fast enough

Elixir doesn't really have locks in the same sense as a C-like language, so you don't really have lock lifetime issues, and Elixir datastructures cannot be modified at all (you can only return new, updated instances of them) so you can't modify them concurrently. Elixir has closures that can capture values from their environment, but since all values in Elixir are immutable, the closure can't modify values that it closes over.

Elixir really is designed for this stuff down to its core, and (in my opinion) it's evident how much better Elixir's design is for this problem space than Go's is if you spend an hour with each. The tradeoff Elixir makes is that Elixir isn't really what I'd call a general purpose language. It's not amazing for CLIs, not amazing for number crunching code, not amazing for throughput-bound problems. But it is a tremendous fit for the stuff most of us are doing: web services, job pipelines, etc. Basically anything where the primary interface is a network boundary.

Edited for formatting.