Clojure core.async — Channels and Go Blocks (CSP Concurrency) | Episode 29
0views
C
CelesteAI
Description
Clojure has one more concurrency story, and it's the one that scales furthest: **CSP-style** — Communicating Sequential Processes. Instead of shared state with locks (atoms, refs, agents), you build producers and consumers as independent lightweight processes (`go` blocks) that pass messages through channels.
In this episode we pull in `core.async`, create a buffered channel with three slots, write a producer that puts five messages on it, and a consumer `go-loop` that reads until the channel closes. Five messages flow through cleanly, and the consumer exits when it sees `nil` from a closed channel.
Student code: https://github.com/GoCelesteAI/clojure-for-beginners/tree/main/episode29
Every keystroke is shown on screen with generous pauses so you can follow along at your own pace.
What You'll Learn:
- Adding `core.async` via `deps.edn`
- `(chan)` and `(chan n)` — unbuffered vs buffered FIFO queues
- `(go body)` — spin up a lightweight process that **parks** (not blocks) on channel ops
- put-bang / take-bang — parking put/take; **only inside go blocks**
- put-bang-bang / take-bang-bang — blocking put/take; **only outside go blocks** (REPL, main thread)
- `(close! ch)` — end-of-stream signal; consumers see `nil`
- `go-loop` as idiomatic consumer pattern with `when-let`
- Why channels scale further than agents — many producers, many consumers, backpressure
Timestamps:
0:00 - Intro
0:15 - Preview: messages, not locks
0:51 - deps.edn with core.async
1:16 - The code — producer + consumer via a channel
1:38 - Start the REPL
1:52 - Require core.async
2:06 - (chan) — unbuffered
2:13 - The channel object
2:19 - (go (put-bang ch :hello))
2:26 - (take-bang-bang ch) — :hello
2:35 - (chan 3) — buffered
2:41 - Put three and close
2:47 - Take them out — 1, 2, 3
3:07 - Take again — nil (closed)
3:18 - Control-D
3:27 - clj -M:run — the full pipeline
3:44 - Recap
4:28 - What's next: Episode 30
Key Takeaways:
1. A channel is a FIFO queue. Buffered channels let the producer run ahead up to N items; unbuffered channels synchronize each put with its take.
2. `go` blocks are not threads — they're lightweight processes that park on channel ops and resume when the op is ready. One real thread can host hundreds of go blocks.
3. **Inside go: use the single-bang ops (take-bang, put-bang)** — they park. **Outside go: use the double-bang ops (take-bang-bang, put-bang-bang)** — they block a real thread. Getting this wrong pins a thread and defeats the whole model.
4. Closed channels are the exit signal: a take on a closed, empty channel returns `nil`. `go-loop` + `when-let` + `recur` is the idiomatic consumer shape.
5. Channels decouple producers from consumers. Swap a single producer for 10, or a single consumer for 10 — the channel absorbs the rate difference.
Phase 5's last primitive. Next up: the concurrency decision guide — `atom` vs `ref` vs `agent` vs `future` vs core.async, compared.
Taught by CelesteAI. Like and subscribe for more Clojure tutorials!