Concurrency

Concurrency = dealing with lots of things at once (not necessarily doing lots of things at once). Critical for high-throughput systems.


ELI5

Sequential:   You wash dishes, then dry dishes, then put away.
              Total: 30 min.

Concurrent:   You wash plate1 → hand to dryer →
 wash plate 2 → hand to dryer →
              overlap the work.
              Total: 20 min.

Parallel:     You wash dishes, your partner dries, your kid puts away.
              Total: 10 min.
              (requires multiple CPU cores)

Concurrency vs Parallelism

DimensionConcurrencyParallelism
DefinitionStructuring to handle multiple tasks at onceExecuting multiple tasks simultaneously
CPU requirement1 core enoughMultiple cores required
GoalResponsiveness, throughputRaw speed
ExampleAsync I/O, event loopsGPU compute, multiprocessing
Pythonasynciomultiprocessing, threading
GoGoroutines (goroutines are concurrent)GOMAXPROCS > 1

Concurrency Models

1. Threads and Locks

Thread 1 ──▶  acquire(lock) ──▶ critical section ──▶ release(lock)
Thread 2 ───────────▶ blocked ──────────────────────────▶ critical section

Problem: Deadlocks, race conditions, hard to reason about.

import threading
 
counter = 0
lock = threading.Lock()
 
def increment():
    global counter
    with lock:  # critical section
        counter += 1  # not atomic without lock

2. Actor Model

Each actor has its own state, communicates via messages.

Actor: mailbox ──▶ process(message) ──▶ state update
           ▲
           │
 messages │
 │
Actor: mailbox ──▶ process(message) ──▶ state update

No shared state — no locks needed. Examples: Erlang, Akka.

# Erlang-style (pseudo)
def order_actor():
    state = {"orders": []}
    while True:
        msg = receive()
        if msg.type == "add_order":
            state["orders"].append(msg.order)
        elif msg.type == "get_orders":
            reply(state["orders"])

3. CSP (Communicating Sequential Processes)

Channels pass messages between goroutines / coroutines.

// Go
ch := make(chan Order, 10)
 
go func() {
    for order := range ch {
        process(order)
    }
}()
 
ch <- Order{ID: "ord-1"}  // non-blocking send

4. Async / Event Loop

Single thread, event-driven, non-blocking I/O.

import asyncio
 
async def fetch_user(user_id: int) -> dict:
    async with aiohttp.ClientSession() as session:
        async with session.get(f"/users/{user_id}") as resp:
            return await resp.json()
 
async def main():
    # Run concurrently — single thread
    users = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )

Key Primitives

Mutex / Lock

Mutual exclusion — only one thread in critical section.

Semaphore

N concurrent accesses allowed.

import threading
 
semaphore = threading.Semaphore(3)  # 3 concurrent connections
 
def make_request():
    with semaphore:
        # only 3 threads here at once
        return http.get("/expensive-endpoint")

Condition Variable

Wait for a predicate to become true.

Atomic Operations

Lock-free operations on primitive types.

# Python
from atomiclong import AtomicLong
counter = AtomicLong(0)
counter.increment()  # thread-safe, no lock

Channels

Synchronous or buffered message passing.


Concurrency Problems

ProblemWhat It IsSolution
DeadlockThreads waiting on each other foreverLock ordering, timeouts
LivelockThreads actively running but making no progressRandom backoff
Race conditionOutcome depends on timingAtomic ops, locks, actors
StarvationThread never gets CPU timeFair schedulers
Priority inversionLow-priority thread holds lock that high-priority thread needsPriority inheritance

Distributed Concurrency

In distributed systems, you don’t have shared memory — you have distributed coordination.

Distributed Locks

# Redis-based distributed lock
import redis, time
 
def acquire_lock(lock_name, ttl=10):
    return redis.set(f"lock:{lock_name}", "1", nx=True, ex=ttl)
 
def release_lock(lock_name):
    redis.delete(f"lock:{lock_name}")
 
# Usage
if acquire_lock("payment:ord-123"):
    try:
        process_payment("ord-123")
    finally:
        release_lock("payment:ord-123")

Leader Election

# etcd / Consul leader election
# Only one instance becomes leader at a time
# Others watch and take over on failure

Two-Phase Commit (2PC)

Phase 1 (Prepare): Coordinator asks all nodes: "can you commit?"
                    Nodes vote YES/NO, hold locks
Phase 2 (Commit):   If all YES → send commit
 If any NO → send rollback

2PC is rarely used in practice (too slow, blocks on coordinator failure) — Raft/Paxos are preferred.


Go vs Python Concurrency

FeatureGoPython
ModelGoroutines + channelsasyncio + coroutines
ParallelismGOMAXPROCS (real parallelism)multiprocessing
Memory sharingShare memory by communicatingCommunicate via shared memory
CancellationContext propagationasyncio.CancelledError
Blocking I/ONon-blocking via channelsNative async/await

Quick Checklist

□ External calls have timeouts
□ Critical sections protected by locks or atomic ops
□ Distributed locks use a lock manager (Redis/etcd)
□ Leader election for active-passive components
□ Idempotency keys for all retry-able operations
□ Circuit breakers to prevent cascading failures
□ Back-pressure to prevent overload (producer throttling)

Source