Sometimes all you share between processes is a
number: a count of requests, a peak, an "already
done" flag. A GenServer works, but every update is
a message and a wait. :atomics gives you integers
that any process can change directly, with no
process and no messages.
:atomics.new(n, opts) makes an array of n
64-bit integers, all zero. Indexes start at 1.
Say at most 3 jobs may run at once. The tempting
version reads, checks, then adds:
if :atomics.get(slots, 1) < 3, do: :atomics.add(slots, 1, 1)
Two processes can both read 2, both pass the check
and both add: 4 jobs running. Between the read and
the write, anything can happen. (A real race can't
be shown reliably, so it isn't run here.)
add_get/3 adds and returns the new value in one
step that no other process can split. Each caller
gets a different number, so exactly 3 see a value
up to 3. The rest take their add back with sub/3.
compare_exchange(ref, i, expected, new) writes
new only if the value is still expected. It
returns :ok on success, or the value it found. A
one-time flag: ten processes race to run setup, and
exactly one wins.
For anything add can't express, loop: read,
compute, compare_exchange, and if someone changed
the value in between, read again and retry. Keeping
the largest value ever seen:
Atomics aren't owned by a process, unlike an ETS
table. This array is made by a process that then
exits, and it keeps working. It's freed once nothing
references it anymore.
:counters is a cut-down :atomics for counting:
add, sub, get and put, always signed. Its
extra is :write_concurrency: adds from many
schedulers at once get faster, but a get that
runs during other adds may see them in any
order. No add is lost, and a read sees every add
that finished before it.
The default, [:atomics], keeps reads consistent.
Pick :counters for metrics that are mostly
bumped and rarely read; pick :atomics when you
need add_get, compare_exchange or unsigned
values.
:persistent_term, read-mostly globals
:persistent_term stores any term under a key for
the whole node. Reads are very fast: the value is
not copied into the reading process, unlike an ETS
lookup. Keys are shared by all code on the node, so
start yours with your app or module name.
The catch is on writes. Replacing or erasing a
value makes every process on the node scan its
heap for the old one, and any process still using
it gets a full garbage collection that copies it
in. With many processes, the whole node can become
less responsive until they're done. So it's for config-like data that is set
at boot and almost never changes.
A process still holding the old value keeps it
working after the erase; the VM copies it over.
11
81
82
83
84
85
86
87
88
89
90
91
92
holder=spawn(fn->limits=:persistent_term.get(key)send(parent,:got_it)receivedo:(:show->send(parent,{:held,limits}))end)receivedo:(:got_it->:ok)IO.inspect(:persistent_term.erase(key),label:"erased")IO.inspect(:persistent_term.get(key,:gone),label:"after erase")send(holder,:show)receivedo:({:held,limits}->IO.inspect(limits.max_jobs,label:"holder still has max_jobs"))
erased: true
after erase: :gone
holder still has max_jobs: 3
Which shared state when?
GenServer or Agent: state with rules, where
updates must happen one at a time in order.
ETS: many keys, read and written by many
processes, owned by one.
:atomics / :counters: plain integers that
many processes bump: counts, gauges, flags.
:persistent_term: a few values read on every
request and set about once per boot.
Atomics wrap around instead of growing like Elixir
integers. One past the largest value is the
smallest:
12
93
94
95
96
97
98
wrap=:atomics.new(1,signed:true):atomics.put(wrap,1,9_223_372_036_854_775_807)IO.inspect(:atomics.add_get(wrap,1,1),label:"signed max + 1")unsigned=:atomics.new(1,signed:false)IO.inspect(:atomics.sub_get(unsigned,1,1),label:"unsigned 0 - 1")
signed max + 1: -9223372036854775808
unsigned 0 - 1: 18446744073709551615
Pitfall: atomics and counters wrap around
silently at 64 bits, as shown above. An unsigned
gauge that is decremented once too often becomes a
huge number, not -1.
Pitfall: an index outside 1..n raises
ArgumentError. Index 0 is never valid.
Pitfall: a :persistent_term.put/2 in a hot
path (per request, per message) replaces the value
each time and can stall the whole node. Store
rarely-changing data there; put changing data in
ETS or atomics.
Pitfall: an atomics or counters reference only
works on the node that created it. Send it to
another node and it points at nothing there.