Back to Notes

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

threadingasynciomultiprocessing
Concurrency modelOS threadsCooperativeSeparate processes
GIL impactLimited for CPUN/A (single thread)None (separate GIL)
Good forI/O-bound + blocking libsHigh-concurrency I/OCPU-bound
OverheadMedium (thread context switch)LowHigh (process fork)
Shared stateYes (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

PatternUse
ChannelOwnership transfer, pipeline, fan-out, signaling
MutexProtecting 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

OptimisticPessimistic
HowRead + version check on writeLock row on read
Good forLow contentionHigh contention
SQLWHERE version = ? + retrySELECT FOR UPDATE
CostRetry on conflictLock 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

QuestionAnswer
"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