Concurrency Deep Dive
Concurrency Deep Dive
Cross-language synthesis: Go goroutines ↔ Python async/threading ↔ distributed concurrency patterns. Most engineers know one — knowing all three is a rare interview differentiator.
The Mental Model
Single-threaded async → Python asyncio / JavaScript event loop
OS threads → Python threading (but GIL limits CPU work)
M:N green threads → Go goroutines (M goroutines on N OS threads)
Multi-process → Python multiprocessing, fork/exec
Distributed concurrency → Kafka, queues, locks, CRDT
Python Concurrency
The GIL — what it is and when it matters
CPython's Global Interpreter Lock allows only one thread to execute Python bytecode at a time.
When GIL hurts: CPU-bound tasks. Two threads doing matrix math = no speedup. When GIL doesn't matter: I/O-bound tasks. Thread releases GIL while waiting for I/O.
# I/O-bound: threading is fine (GIL released during I/O)
with ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(fetch_url, urls))
# CPU-bound: use multiprocessing (separate processes, no shared GIL)
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(heavy_compute, data))
asyncio — cooperative concurrency
Single thread, single event loop. Coroutines yield control explicitly with await.
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.json()
async def main():
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*[fetch(session, u) for u in urls])
When to use async: Many concurrent I/O operations (HTTP, DB queries, file reads). When NOT to use async: CPU-bound work (still single thread), simple scripts, mixed sync/async codebases (contagious — forces refactor).
threading vs asyncio vs multiprocessing
| threading | asyncio | multiprocessing | |
|---|---|---|---|
| Concurrency model | OS threads | Cooperative | Separate processes |
| GIL impact | Limited for CPU | N/A (single thread) | None (separate GIL) |
| Good for | I/O-bound + blocking libs | High-concurrency I/O | CPU-bound |
| Overhead | Medium (thread context switch) | Low | High (process fork) |
| Shared state | Yes (with locks) | Yes (single thread, safe) | No (use Queue/Pipe) |
Resources: [[Python/Language Core/Python Programming]]
Go Concurrency
Goroutines — M:N green threads
Go runtime schedules M goroutines onto N OS threads. Stack starts at 2KB, grows dynamically (vs 8MB for OS thread). Spin up thousands of goroutines cheaply.
// Goroutine: just add `go`
go func() {
result := heavyCompute()
ch <- result
}()
vs Python threading: No GIL. Goroutines are truly concurrent for CPU-bound work (multiple OS threads). Stack is much smaller. Scheduling is in userspace (Go runtime), not OS.
Channels — typed communication pipes
Channels force explicit synchronization. "Don't communicate by sharing memory; share memory by communicating."
ch := make(chan int) // unbuffered: sender blocks until receiver ready
ch := make(chan int, 10) // buffered: sender blocks only when full
// Direction constraints (at compile time):
func producer(out chan<- int) { out <- 42 } // send-only
func consumer(in <-chan int) { v := <-in } // receive-only
select — multiplex channels
select {
case msg := <-ch1:
// handle ch1
case msg := <-ch2:
// handle ch2
case <-time.After(5 * time.Second):
// timeout
}
sync.Mutex vs channels — when to use which
| Pattern | Use |
|---|---|
| Channel | Ownership transfer, pipeline, fan-out, signaling |
| Mutex | Protecting shared state (cache, counter, map) |
var mu sync.RWMutex
var cache = map[string]string{}
func get(key string) string {
mu.RLock()
defer mu.RUnlock()
return cache[key]
}
Resources: [[Go/Channels]], [[Go/Mutexes]]
Common Patterns — Cross-Language
Fan-out / Worker Pool
Python:
with ThreadPoolExecutor(max_workers=N) as pool:
futures = [pool.submit(task, item) for item in items]
results = [f.result() for f in futures]
Go:
jobs := make(chan Job, len(items))
results := make(chan Result, len(items))
for i := 0; i < workers; i++ {
go worker(jobs, results)
}
for _, item := range items { jobs <- item }
close(jobs)
for range items { <-results }
Rate Limiting / Throttling
Python (asyncio): asyncio.Semaphore(N) to cap concurrent coroutines.
Go: Buffered channel as semaphore — sem := make(chan struct{}, N).
Distributed: Redis token bucket with Lua script. See: [[System Design/Problem Designs/Rate Limiter]]
Pub/Sub / Event Bus
Python: asyncio.Queue for in-process, Kafka/Redis pub-sub for distributed.
Go: Channel broadcasting (one producer → multiple goroutine consumers via fan-out).
Distributed: Kafka, SNS/SQS. See: [[System Design/Problem Designs/Notification System]]
Distributed Concurrency
The CAP Theorem
Consistency + Availability + Partition Tolerance — pick 2.
- CP: strong consistency, block on partition (ZooKeeper, etcd)
- AP: available on partition, eventual consistency (Cassandra, DynamoDB)
See: [[Distributed Systems Concepts]]
Distributed Locking
- Redis SETNX + TTL: simple advisory lock. Problem: clock skew, process crashes.
- Redlock: Acquire lock on N/2+1 Redis nodes. Controversial (Martin Kleppmann critique).
- ZooKeeper/etcd: consensus-based, stronger guarantees.
Optimistic vs Pessimistic Locking
| Optimistic | Pessimistic | |
|---|---|---|
| How | Read + version check on write | Lock row on read |
| Good for | Low contention | High contention |
| SQL | WHERE version = ? + retry | SELECT FOR UPDATE |
| Cost | Retry on conflict | Lock overhead |
CRDT — Conflict-free Replicated Data Types
Operations commute — merge without conflict. Used in collaborative editing (Google Docs), distributed counters.
- G-Counter: only increments, merge = max per node
- LWW-Register: last-write-wins by timestamp See: System Design — Google Docs problem (upcoming W09)
Interview Cheat Sheet
| Question | Answer |
|---|---|
| "What's the GIL?" | CPython single-execution lock. I/O-bound: use threading/async. CPU-bound: use multiprocessing. |
| "Goroutine vs thread?" | M:N green threads. 2KB stack vs 8MB. Scheduled by Go runtime. Thousands are cheap. |
| "Channel vs mutex?" | Channel for ownership transfer/signaling. Mutex for protecting shared state. |
| "Async vs threaded?" | Async: cooperative, one thread, great for many I/O ops. Threading: preemptive, parallel I/O (but GIL limits CPU). |
| "How to avoid race condition?" | Identify shared mutable state. Go: channels or sync.Mutex. Python: asyncio (single-thread safe), threading.Lock, or make state immutable. |
Related
- [[Go/Channels]] — channel patterns in depth
- [[Go/Mutexes]] — sync.Mutex and RWMutex
- [[System Design/Backend 101/Concurrency/Concurrency]] — backend concurrency patterns
- [[Distributed Systems Concepts]] — CAP, consensus, replication
- [[System Design/Problem Designs/Rate Limiter]] — distributed rate limiting
- [[synthesis/Tech Stack Overview]] — positioning guide