Chapter 10: The Indivisible Moment
Tuesday morning brought a sharp, biting wind off the Hudson, rattling the old sash windows of the library archive. Ethan arrived shaking the cold from his coat, carrying a cardboard tray with two coffees and a small, wax-paper bag.
Eleanor was already at the desk, her reading glasses perched on the end of her nose, reviewing a stack of printouts. She sniffed the air. "Blueberry?"
"Close. Huckleberry scones from a place in the West Village. And a Geisha pour-over, black."
Eleanor accepted the coffee with a rare, genuine smile. "Excellent. You’re learning that precision matters in coffee just as much as in code."
Ethan pulled up a chair. "Speaking of precision, I was thinking about last week. We fixed the data race in the counter …
Chapter 10: The Indivisible Moment
Tuesday morning brought a sharp, biting wind off the Hudson, rattling the old sash windows of the library archive. Ethan arrived shaking the cold from his coat, carrying a cardboard tray with two coffees and a small, wax-paper bag.
Eleanor was already at the desk, her reading glasses perched on the end of her nose, reviewing a stack of printouts. She sniffed the air. "Blueberry?"
"Close. Huckleberry scones from a place in the West Village. And a Geisha pour-over, black."
Eleanor accepted the coffee with a rare, genuine smile. "Excellent. You’re learning that precision matters in coffee just as much as in code."
Ethan pulled up a chair. "Speaking of precision, I was thinking about last week. We fixed the data race in the counter using a Mutex. It worked, but..."
"But?" Eleanor broke a piece of the scone.
"It felt heavy. Locking, unlocking, deferring... just to add one to a number? Is there a faster way? Something... smaller?"
Eleanor wiped a crumb from her notebook. "There is. But it requires you to think less like a programmer and more like a CPU."
She opened her laptop. "Remember when I told you counter++ isn’t one step? It’s three: read the value, add one, write it back. A Mutex protects all three steps by freezing the world around them. But modern processors have instructions that can do all three steps instantly. Indivisibly."
"Indivisibly?"
"Atomically. From the Greek atomos—uncuttable. An operation that cannot be interrupted."
Eleanor sketched a quick diagram in the margin of her notes. "Think of it like locking the memory bus itself, just for a single clock cycle."
NORMAL EXECUTION ATOMIC EXECUTION
[CPU Core 1] [CPU Core 1]
| |
| Read 0 | AtomicAdd(1)
| Add 1 | (Bus Locked 🔒)
| Write 1 | (Value becomes 1)
| | (Bus Unlocked 🔓)
[CPU Core 2] [CPU Core 2]
| |
| (Interferes!) | (Must Wait)
"See?" She pointed with her pen. "In the atomic version, Core 2 physically cannot interfere. The hardware prevents it."
Ethan nodded. "Okay, I see the theory. What does it look like in Go?"
Eleanor opened a new file.
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
var counter int64
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
// Atomic increment
atomic.AddInt64(&counter, 1)
}
}()
}
wg.Wait()
fmt.Printf("Counter: %d\n", counter)
fmt.Printf("Time taken: %v\n", time.Since(start))
}
She ran it.
Counter: 50000
Time taken: 1.25ms
"No Mutex," Eleanor said softly. "atomic.AddInt64 uses that hardware instruction we just drew. It is the rawest form of synchronization."
Ethan looked at the code. "It looks simple. Why don’t we use this for everything?"
"Because it only works for simple things. Integers. Pointers. Booleans. You can’t atomically append to a slice or update a map. But for counters, gauges, or flags? It is orders of magnitude faster. A Mutex might cost you 30 nanoseconds. An atomic add? Maybe one or two."
"And no sleeping?"
"Never. A Mutex involves the Go runtime scheduler—it might put a goroutine to sleep. An atomic operation happens entirely in hardware. It never sleeps. It spins or it succeeds."
Eleanor took a sip of the Geisha. "But there’s a catch. You have to be careful how you read the data, too. You can’t just print counter directly while others are writing to it, or you might see a ‘torn read’—half the bits from the old value, half from the new."
She modified the code:
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var value int64
// Writer
go func() {
for {
atomic.AddInt64(&value, 1)
time.Sleep(time.Millisecond)
}
}()
// Reader
for i := 0; i < 5; i++ {
// Safe atomic read
current := atomic.LoadInt64(&value)
fmt.Println("Current value:", current)
time.Sleep(2 * time.Millisecond)
}
}
"Note atomic.LoadInt64. Just as you must write atomically, you must read atomically. This ensures you get a consistent snapshot of the memory."
Ethan nodded slowly. "So, Add to write, Load to read. What else?"
"Store," Eleanor said. "And the most powerful one of all: Compare and Swap."
She opened a fresh editor window. "Imagine you want to change a value, but only if it hasn’t changed since you last looked at it. This is the foundation of ‘lock-free’ algorithms."
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var status int32 = 0 // 0 = idle, 1 = busy
// Try to set status to busy
swapped := atomic.CompareAndSwapInt32(&status, 0, 1)
if swapped {
fmt.Println("Success: Status changed from 0 to 1")
} else {
fmt.Println("Failure: Status was not 0")
}
// Try again
swapped = atomic.CompareAndSwapInt32(&status, 0, 1)
if swapped {
fmt.Println("Success: Status changed from 0 to 1")
} else {
fmt.Println("Failure: Status was already 1")
}
}
Output:
Success: Status changed from 0 to 1
Failure: Status was already 1
"CompareAndSwap—CAS for short. You say: ‘I think the value is 0. If it is, change it to 1. If it isn’t, tell me.’ This allows multiple goroutines to coordinate without ever putting a lock on the door. They just keep trying until they succeed."
Ethan rubbed his chin. "This feels... dangerous."
Eleanor chuckled, a dry sound. "It is. Atomics are sharp knives. With channels, the design is obvious. With Mutexes, the critical section is clear. With atomics, you are managing memory states manually. If you mix atomic and non-atomic access to the same variable, you will crash. If you don’t align your memory correctly on 32-bit architectures, you will crash."
She turned to look at him directly. "Ethan, here is the rule: Use channels to orchestrate. Use Mutexes to protect complex state. Use Atomics only when you need raw speed for simple counters or state flags."
"So, don’t be a hero?"
"Exactly. Clever code is hard to debug. Atomic code is impossible to debug."
She tapped a final example into the laptop. "There is one high-level atomic tool that is safe and incredibly useful, though. atomic.Value. It handles any type, not just numbers. It’s perfect for things like updating a configuration while the server is running."
package main
import (
"fmt"
"sync/atomic"
"time"
)
type Config struct {
APIKey string
Timeout time.Duration
}
func main() {
var config atomic.Value
// Initial config
config.Store(Config{APIKey: "INITIAL", Timeout: time.Second})
// Background updater
go func() {
for {
time.Sleep(100 * time.Millisecond)
// Atomically replace the entire struct
config.Store(Config{
APIKey: "UPDATED",
Timeout: 2 * time.Second,
})
}
}()
// Reader loop
for i := 0; i < 3; i++ {
// Atomically load the entire struct
// WARNING: This type assertion panics if the type is wrong
c := config.Load().(Config)
fmt.Printf("Current Config: Key=%s Timeout=%v\n", c.APIKey, c.Timeout)
time.Sleep(40 * time.Millisecond)
}
}
Output:
Current Config: Key=INITIAL Timeout=1s
Current Config: Key=INITIAL Timeout=1s
Current Config: Key=UPDATED Timeout=2s
"See? config.Load() returns the latest full configuration. config.Store() replaces it entirely. No locks, no blocking readers. The readers always see a complete, valid Config object—either the old one or the new one, never a mix. Just be careful with that type assertion—atomic.Value stores an any, so Go won’t check the type for you until runtime."
Ethan finished his scone. "That’s actually really clean. atomic.Value for configuration, atomic.Add for metrics."
"Precisely," Eleanor closed her laptop. "You are building a toolbox, Ethan. Channels are your power drills. Mutexes are your vices. Atomics... they are the scalpel. You don’t use a scalpel to build a house, but when you need surgery, nothing else will do."
She picked up her cup. The coffee was cooling, but she took a long, satisfied sip.
"Next time," she said, "we step back from the hardware. We need to talk about how to structure all this code. Packages, visibility, and how to build a program that doesn’t collapse under its own weight."
Ethan gathered the trash. "Project structure?"
"Project structure. The architecture of a Go application. It’s time you stopped writing scripts and started building systems."
Key Concepts from Chapter 10
Atomic Operations: Operations that execute indivisibly. They cannot be interrupted by other goroutines or CPU instructions. Found in the sync/atomic package.
atomic.AddInt64: Atomically increments a variable. Much faster than a Mutex for simple counters because it uses CPU hardware instructions rather than OS-level locking.
atomic.Load / atomic.Store: Safely reading and writing values without locks. Prevents "torn reads" where you might see partially written data.
Compare and Swap (CAS): atomic.CompareAndSwapInt32(&addr, old, new). Atomically updates a value only if it currently matches the "old" value. The basis for lock-free algorithms.
atomic.Value: A type that can atomically load and store values of any type (structs, maps, etc.). Ideal for "read-heavy, write-rarely" scenarios like updating global configuration.
Type Safety: atomic.Value stores any (interface{}). Always verify your type assertions when loading, as a mismatch will cause a runtime panic.
The Hierarchy of Synchronization:
- Channels: Use for communication and passing ownership of data.
- Mutex: Use for protecting critical sections and complex shared state.
- Atomic: Use for simple counters, flags, and high-performance metrics.
Memory Safety: Do not mix atomic and non-atomic access to the same variable. If you use atomic.Store, you must use atomic.Load to read it.
Next chapter: Project Structure and Packages—where Eleanor explains how to organize code, the internal directory, and how to design clean APIs.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.