Best examples of concurrency?

Forgive an open-ended question, but I’m curious as to what the community thinks are some of the best examples of how concurrency can be helpful? Like – what are the best “recipes” you have come across?

What I’m trying to get at is what about concurrency specifically makes Elixir such a good choice. Are there specific functions (e.g. from the Task module) that you reach to regularly to handle certain problems?

Thanks for any thoughts!

I can give you more examples Later™ but to me Task.async_stream has been a game changer ever since I learned Elixir for the first time (almost exactly 8 years ago). It’s a beautiful way to do scatter-gather computing.

There are many embarrassingly parallel computing problems and Elixir enables you to tackle them almost transparently.

In my case I was rewriting a Ruby reporting system and there you have to process millions of separate records. Elixir lent itself absolutely perfectly to splitting these records into buckets of 1000-2000 records each chunk and process each of them in parallel. I also used ETS to aggregate some of the results and it proved itself extremely capable and was not a bottleneck even though my production code was having something like 500 separate processes update various records in an ETS table at the same time (+ counters).

Another very common pattern is task pools, of which Oban is the logical conclusion, though there is space for many more solutions on a smaller scale that don’t require DB persistence. I’d even venture to say that you haven’t properly practiced Elixir until you have written a small task pooling library for yourself.

Oh, and also DynamicSupervisor.start_child + Registry. A match made in heaven. It helps you monitor 3rd party “live” entities, like connections to user’s devices for example (something LiveView makes use of as well, though not specifically with those modules).

2 Likes

Concurrency makes sure a large workload cannot block other concurrent workloads from making progress towards succeeding. This is needed everywhere in multi user systems – where there’s not just a single user (or you can live with the uncapped latency of sequential processing of input).

A classic example is a website where one user triggers a heavy report generation, where you don’t want other users to wait for the report to have finished before handling their requests.

Though again this comes up everywhere where you need to make progress on multiple tasks without doing it sequentially. Webservers, DB connection pools, processing pipelines and many more. Even if a specific task at hand (like processing events) might work fine being done sequentially it might still require running concurrently to different tasks of the system like sending logs off to somewhere, error reporting, some management dashboard, debugging tools and again many more.

Implementing something like this is not impossible in other ecosystems, but requires (sometimes a lot) more work to be implemented and especially to get correct. Also see virdings first law of programming.

That’s where processes, the share nothing/no global state approach and message passing of the beam round out the picture. They’re a great abstraction to be used for dividing up work between “this code meant to run sequentially as programmed, though concurrently with other processes” from “this is now a separate process, which can run concurrently to what spawned it”. This abstraction and how it can be used to model/build up larger things to do the pervious mentioned more concrete things is to me the core distinction to other languages.

And then the ability to parallelize with multiple cpu cores and move work across a cluster of nodes almost falls out for free from the pritimives.

So if you want to look at actual elixir examples:

  • Look at bandit and how it models http handling with processes. There’s a pool of acceptors (responding to the initial tcp connection coming in) and then later a process per connection and/or request (different between http versions) to handle the actual http request
  • Look at DBConnection for how ecto db connections are pooled
  • Look at Task.async_streamGenStageFlow to see how you can climb the ladder of abstraction on process pipelines, even though underneight it’s still mostly a bunch of processes arranged in a useful manner.
  • Look at Finch for pooling outgoing http request

Often you’ll not interact directly with many of these because they’re part of some library and the users of such don’t need to care, but sometimes it’s not worth pulling in a library when e.g. Task.async_stream does well enough.

4 Likes