Clojure agent — Asynchronous State with send, await, and Background Updates | Episode 27
0views
C
CelesteAI
Description
Some state changes shouldn't block the caller. Log lines, metrics, cache warmers — you queue them and get back to work. Clojure's `agent` is built for exactly that: a container for one value, updated asynchronously on a background thread.
You `send` a pure function to the agent. Clojure queues it, runs it on an agent thread, and the agent's value becomes whatever your function returns. Your caller returned immediately — the whole point. When you need to know the sends have landed, `await` blocks until they have.
In this episode we build a tiny logger, watch `send` decouple the caller from the work, and hammer the agent with a thousand concurrent sends. Every entry lands, in order.
Student code: https://github.com/GoCelesteAI/clojure-for-beginners/tree/main/episode27
Every keystroke is shown on screen with generous pauses so you can follow along at your own pace.
What You'll Learn:
- `(agent initial-value)` — create an agent
- `@agent` / `(deref agent)` — read the current value (non-blocking)
- `(send a f & args)` — queue `(apply f current-value args)` on the agent thread pool; returns immediately
- `(send-off a f & args)` — same, but for blocking/IO fns (separate unbounded thread pool)
- `(await a)` — block until sends queued from this thread have completed
- `(shutdown-agents)` — release the agent thread pools (required at end of -main)
- atom vs ref vs agent — when to pick which
Timestamps:
0:00 - Intro
0:15 - Preview: fire-and-forget state
0:51 - The logger agent + log! helper
1:10 - Start the REPL, load the namespace
1:35 - @logger — an empty vector
1:43 - send — queue conj, caller returns immediately
1:50 - @logger — the conj already ran
1:58 - log! two more entries
2:10 - await — block until sends finish
2:17 - All three entries in order
2:26 - Five more sends in a loop — no waiting
2:40 - After await, all five are there
2:51 - Control-D
3:06 - clj -M:run — 1000 async sends, all landing
3:17 - Recap
3:55 - What's next: Episode 28
Key Takeaways:
1. An agent is one value, updated asynchronously on its own thread.
2. `send` queues a fn; the caller returns *now* and the fn runs later on the agent thread.
3. Sends from the same thread always run in the order you sent them — no retries, no race conditions on this agent's value.
4. `await` is your synchronization point: block until every send queued from this thread has committed.
5. Agent fns can have side effects (unlike atom's swap! fn) — the agent runs them serially, so there's no "called twice" concern.
6. End your `-main` with `(shutdown-agents)` — otherwise the agent thread pools keep the JVM alive.
Phase 5 continues. Next up: `future` and `promise` — async *computation* (vs. async *state*) with one-shot values.
Taught by CelesteAI. Like and subscribe for more Clojure tutorials!