Erlang
parallel processing
asynchronous programming
concurrency
code optimization

How to make parallel calls in Erlang and wait for all of the results?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Erlang, the normal way to run work in parallel is to spawn lightweight processes, send their results back to the parent, and then collect those messages with receive. The pattern is simple, but it works well because Erlang processes are cheap and message passing is the default coordination mechanism.

The Core Pattern

The parent process does three things:

  1. remembers its own pid with self()
  2. spawns one worker per task
  3. waits until every worker has replied

A minimal implementation looks like this:

erlang
1-module(parallel_demo).
2-export([parallel_map/2]).
3
4parallel_map(Fun, Items) ->
5    Parent = self(),
6    Pairs = [
7        {Item,
8         spawn(fun() ->
9             Result = Fun(Item),
10             Parent ! {self(), Item, Result}
11         end)}
12|| Item <- Items ], gather(length(Pairs), []). gather(0, Acc) -> lists:reverse(Acc); gather(N, Acc) -> receive {_Pid, Item, Result} -> gather(N - 1, [{Item, Result} | Acc]) end. ``` You can call it like this from the Erlang shell: ```erlang 1> c(parallel_demo). 2> parallel_demo:parallel_map(fun(X) -> X * X end, [1,2,3,4]). [{1,1},{2,4},{3,9},{4,16}] ``` Each item is processed in a separate Erlang process, and the parent blocks until it has received all replies. ## Why This Works Well in Erlang Erlang is designed around massive concurrency. Processes are isolated and communicate only by messages, so parallel task orchestration is much lighter than thread-based designs in many other runtimes. This means you usually do not think in terms of locks. You think in terms of: - who should do the work - who should receive the result - how failures should be handled That is why the spawn-plus-receive pattern is so common. ## Preserving Input Order The first example gathers results in arrival order, not input order. If task duration varies, the output order may differ from the original list. If you need stable input order, attach an index to each task and sort the results after collection. ```erlang parallel_map_ordered(Fun, Items) -> Parent = self(), Indexed = lists:zip(lists:seq(1, length(Items)), Items), [spawn(fun() -> Parent ! {Index, Fun(Item)} end) || {Index, Item} <- Indexed], Results = gather_ordered(length(Indexed), []), [Result || {_Index, Result} <- lists:keysort(1, Results)]. gather_ordered(0, Acc) -> Acc; gather_ordered(N, Acc) -> receive {Index, Result} -> gather_ordered(N - 1, [{Index, Result} | Acc]) end. ``` That pattern is useful when the result list must align with the original request list. ## Handling Failures Better Plain `spawn` is fine for basic examples, but real systems often want stronger failure semantics. If a worker crashes, the parent waiting for all results can hang forever unless you add timeouts or monitors. A practical improvement is `spawn_monitor`, which gives the parent a `DOWN` message if the child dies. That lets you decide whether to: - retry the task - mark it failed - stop the whole operation For a quick timeout safeguard, add an `after` clause in `receive`. ## Common Pitfalls The most common mistake is forgetting that message arrival order is not the same as input order. If order matters, carry an index. Another mistake is spawning workers and waiting forever without any timeout or crash handling. One failed child can otherwise block the parent indefinitely. A third pitfall is doing huge numbers of CPU-heavy tasks without thinking about system load. Erlang processes are cheap, but the work they perform still consumes scheduler time. ## Summary - Spawn one Erlang process per task and have each worker send a result message back to the parent. - Use `receive` to collect replies until all expected results arrive. - Add indexes if you need results returned in input order. - For production code, add timeouts or monitors so worker crashes do not hang the parent. - Erlang makes this pattern natural because lightweight processes and message passing are built into the runtime model.

Course illustration
Course illustration

All Rights Reserved.