Concurrency, Synchronization & Deadlock Questions
Coordinating concurrent execution against shared state: mutexes, semaphores, condition variables, spinlocks, and atomic operations, plus the problems they solve and cause. Covers race conditions, critical sections, the classic deadlock conditions and prevention/avoidance strategies, priority inversion, and livelock at the OS level.
Multiple microservices have circular dependencies and during a rolling deployment you observe availability loss because service A waits on B and B waits on A. Design a deployment and synchronization strategy to avoid such deadlocks or availability gaps during concurrent upgrades. Address schema migrations, API compatibility, orchestration, and automated safety checks.
Sample Answer
Requirements & constraints:
- Zero or minimal user-visible downtime during rolling upgrades
- Multiple services with circular runtime dependencies
- Support schema and contract (API) changes with backward/forward compatibility
- Automated orchestration and safety gates for rollback
High-level strategy:
- Decouple runtime dependencies with feature flags + graceful degradation
- Enforce backward- and forward-compatible changes (expandable schema, versioned APIs)
- Use canary/blue-green + orchestrator-aware rollout with dependency-aware ordering
- Automated safety checks (pre-rollout and runtime) and automated rollback policies
Detailed plan:
- API & Schema rules
- Follow compatibility rules: additive-only DB/schema migrations (nullable new columns, new tables), use views or adapters for deletes/renames; for API, prefer additive fields and versioned endpoints (/v2) for breaking changes.
- Two-phase migrations for breaking DB changes: deploy code that reads old+new schema, write to both (backfill), switch readers, then drop legacy artifacts in a later release.
- Deployment ordering & orchestration
- Build a dependency graph and label services with “upgrade-stage” constraints.
- Orchestrator (Kubernetes + Argo Rollouts/Flagger) runs staged rollout: first deploy consumer-compatible changes (clients tolerant), then providers. For A<->B cycle, break cycle by upgrading one side to tolerant mode first (feature flag to use new contract only when available).
- Use canaries (small % traffic) and progressive rollout with manual/automated promotion.
- Synchronization & runtime safety
- Implement health-check probes that include dependency readiness (but with timeouts and fallback) to avoid blocking pod readiness indefinitely.
- Circuit breakers and retries with exponential backoff so transient unavailability during upgrade doesn’t cascade.
- Automated safety checks
- Pre-deploy: contract tests (consumer-driven pact tests), migration dry-run on staging snapshot, static analysis for compatibility.
- During deploy: synthetic transactions and smoke tests against canaries; monitor latency, error-rate, dependency-specific SLIs.
- Define SLO-based automated rollback: if error-rate or latency breaches thresholds or downstream readiness drops, rollback the canary automatically.
- Rollback & observability
- Keep fast rollback path (retain prior image, DB backward-compatible for N releases).
- Strong observability: distributed tracing, dependency maps, dashboards for per-service availability and inter-service error rates.
- Playbook & governance
- Maintain migration/upgrade checklist, require DB migration approvals and mark schema migrations as reversible or two-phase.
- Automate gating in CI/CD to block merges that violate compatibility rules.
Why this works:
- Two-phase migrations + versioned APIs prevent simultaneous incompatible states.
- Feature flags and consumer-driven contracts let you stagger behavioral changes to break circular waits.
- Orchestrator-driven canaries plus automated SLO-based rollbacks provide fast detection and recovery, minimizing user impact.
Explain what a semaphore is, including the difference between binary and counting semaphores, the typical operations (wait/P and signal/V), and at least two realistic SRE use cases (for example, limiting concurrent database connections or coordinating access to a pool). Mention fairness and common pitfalls when using semaphores in production systems.
Sample Answer
A semaphore is a synchronization primitive that controls concurrent access to shared resources by maintaining a counter. Typical operations are wait/P (decrement; block if counter is zero) and signal/V (increment; wake a waiting thread). Semaphores can be implemented in-kernel or user-space with condition variables.
Binary vs counting:
- Binary semaphore: counter ∈ {0,1}. Acts like a mutex for exclusive access.
- Counting semaphore: counter ≥ 0. Allows up to N concurrent holders (useful for resource pools).
SRE use cases:
- Limiting concurrent DB connections: place a counting semaphore with capacity equal to the DB pool size in front of worker threads or server handlers to prevent overload and excessive connection churn.
- Coordinating access to a finite resource pool: e.g., a pool of license-limited services, GPUs, or API tokens—threads acquire semaphore before using a resource and release after.
- Throttling background jobs or deployments: limit parallel rollout workers to protect downstream systems.
- Producer/consumer coordination: use semaphores to signal available items or free buffer slots in a queue.
Fairness and pitfalls:
- Many semaphore implementations are not fair: wake order can be unpredictable, causing starvation of some threads. If fairness matters (e.g., per-request SLA), use FIFO queues or fair locks.
- Deadlocks: combining semaphores with other locks can create circular waits—always document acquisition order or use timeouts.
- Resource leaks: failing to signal on error paths permanently reduces capacity; use try/finally or RAII patterns to ensure release.
- Overuse for complex logic: semaphores are low-level—higher-level constructs (worker pools, rate limiters) are often safer and easier to reason about.
- Monitoring: expose metrics (current holders, waiters, acquisition latency) and alert when wait times spike so you can tune capacity rather than just raising limits.
In production, prefer clear ownership, timeouts, and observability when deploying semaphores to avoid subtle availability impacts.
Design a distributed counting semaphore using Redis or ZooKeeper to coordinate access to a limited external resource among many clients. Describe how you would implement acquire/release, handle client crashes or lease expirations while holding permits, and discuss fairness and performance trade-offs.
Sample Answer
Requirements & constraints:
- N clients, M total permits (M small relative to clients), low latency, high availability, survive client crashes, optional fairness (FIFO), lease/timeout for stuck permits.
High-level approaches:
- ZooKeeper-based: use ephemeral sequential znodes to implement FIFO queue + counting.
- Redis-based: use atomic Lua scripts + sorted set or list with TTL/lease tokens.
ZooKeeper design (preferred for strict correctness/fairness):
- Namespace /semaphores/<name> with child ephemeral sequential nodes created by clients.
- Acquire: client creates ephemeral sequential node; then reads children count ordered by sequence. If its index <= M, it holds a permit; else it watches the (index − M) node to be notified when it disappears.
- Release/crash: client deletes its node on release; if client crashes its ephemeral node automatically removed by ZK session expiration — freeing permit.
- Lease handling: tune session timeout; allow explicit renewals by heartbeats.
- Fairness: sequential nodes give FIFO fairness. Performance: ZK serializes writes to the leader; good for correctness but lower throughput at very high QPS.
Redis design (lighter-weight, higher throughput):
- Use a sorted set "sem:<name>" where members are unique token IDs with score = expiry timestamp.
- Acquire (Lua script atomically):
- Remove expired tokens (ZREM by score < now).
- If ZCARD < M: ZADD token with score = now + lease; return token.
- Else return failure + optionally the earliest expiry for backoff.
- Release (Lua): ZREM token.
- Crash handling: clients set short leases and must renew before expiry (heartbeat). If client dies, token auto-expired and can be reclaimed.
- Fairness: Redis approach is not FIFO; you can add a queue list to implement FIFO (push requester id then pop M and grant tokens), but that adds complexity and extra round-trips.
- Performance: Redis offers higher throughput and lower latency; Lua scripts keep operations atomic. Watch for clock skew across clients/servers — use Redis server time for expiry.
Trade-offs & operational considerations:
- Correctness vs performance: ZK gives strong semantics and built-in crash detection (ephemeral nodes), Redis gives speed but requires careful TTL/renewal logic.
- Lease length: must balance false releases (too-short) vs stuck permits (too-long).
- Heartbeat/renewal: implement exponential backoff, jitter, and exponential backoff for retries to avoid stampedes.
- Monitoring: expose metrics — held permits, expired releases, queue length, acquire latencies, failed renewals. Alert on high stuck-permit rate or ZK session expirations.
- Scalability: shard semaphores by name, use Redis Cluster or multiple ZK ensembles for isolation.
- Security: authenticate clients and use ACLs.
Summary: Use ZooKeeper for FIFO fairness and robust crash semantics; use Redis + Lua for higher throughput but add lease/renew and monitoring to avoid lost or stolen permits.
How can heap and CPU profiling help diagnose synchronization bottlenecks such as hot locks or contention hotspots? Describe the metrics to look for, how to attribute time blocked on locks to call stacks, and name Linux and language-specific tools you would use in production to gather this information with minimal overhead.
Sample Answer
Start with what you want to measure and why: synchronization bottlenecks show up as threads/green-threads spending significant time waiting for locks (high lock wait time), long mutex hold times, high lock acquisition rate, increased context switches, reduced throughput and elevated tail latencies. Combine CPU (where time is spent) and heap/allocation profiling (where allocations or GC may cause lock pressure on allocators or runtime locks).
Metrics & signals to look for
- Lock wait time per lock (total and average): high values indicate hot locks.
- Mutex hold time (max/avg): long critical sections.
- Contention count / failed try-locks.
- Threads blocked count and stack traces.
- Voluntary context switches / sched switches (pidstat -w).
- Increased allocations / GC pause time (heap profile) that correlate with contention.
How to attribute blocked time to call stacks
- Use profilers that sample stacks at the moment of blocking or record stack at lock acquisition. That gives call paths leading to contention (caller holding lock, callers trying to acquire).
- Off-CPU (blocked) profiling: collect stacks when a thread is not running (waiting). Correlate with sched_switch tracepoints to see who was running before/after wait.
- Example approach:
- Enable mutex/block profiling in the runtime (language support) so the profiler emits stack traces for both holder and waiter.
- Capture CPU profiles and off-CPU profiles in parallel, then generate flamegraphs that show on-CPU time and off-CPU (wait) time per stack.
- Use sampling (low overhead) and aggregation to attribute wall-clock blocked time to the code paths.
Tools (Linux + language-specific) with low overhead
- Linux-level:
- perf (perf record -g -p PID; perf lock record/annotate): CPU sampling + lock events; perf lock helps analyze futex/mutex contention.
- perf sched / tracepoints: analyze sched_switch and context switches.
- bpftrace / BCC tools (offcputime, offcputime.py, funccount/uprobe): lightweight eBPF off-CPU and lock tracing with low overhead.
- pidstat -w / top -H / /proc/locks for quick signals.
- Go:
- net/http/pprof and runtime/pprof: CPU, heap, mutex (runtime.SetMutexProfileFraction), block profile (GOMAXPROCS-aware). pprof provides allocation/mutex/block stacks and flamegraphs.
- go tool pprof -http=: CPU and block/mutex profiles.
- Java:
- async-profiler (perf_events based): CPU and alloc and lock profiler with low overhead, produces flamegraphs.
- jcmd/jstack + async-profiler for live stacks.
- Python:
- py-spy (sampling, works with native threads), pyinstrument; for blocking use eBPF off-CPU or Py-Performance hooks (e.g., faulthandler) and tracemalloc for allocations.
- C/C++:
- perf, perf lock, gperftools heap profiler, heaptrack for allocations, and eBPF uprobe/tracepoints.
- Containerized/production considerations:
- Use sampling profilers (perf, async-profiler, eBPF) at low frequency to keep overhead minimal.
- Collect short-duration profiles during degraded behavior windows or use continuous low-sample-rate collection aggregated centrally.
- Avoid heavy instrumentation (full tracing or debug builds) on production hot paths unless in a controlled incident.
Concrete example commands
- perf record CPU sampling with callgraphs:
perf record -F 99 -g -p <PID> -- sleep 30
perf report --call-graph - perf lock (contention analysis):
perf lock record -p <PID> sleep 30
perf lock report - eBPF off-CPU aggregation (bcc):
sudo /usr/share/bcc/tools/offcputime -p <PID> 30 - Go mutex/block profiles:
runtime.SetMutexProfileFraction(1)
http://localhost:6060/debug/pprof/mutex and /debug/pprof/block
Key practices
- Correlate heap allocation hotspots with mutex/block profiles — heavy allocation sites can cause allocator locks or GC pauses that look like sync issues.
- Capture both holder and waiter stacks to decide whether to shrink critical sections, change lock granularity, switch to lock-free structures, or apply sharding.
- Start with low-overhead sampling and escalate to targeted tracing only when needed.
This combination reliably surfaces hot locks: CPU/blocked stack traces point to who’s waiting, mutex/block profiles show which locks, and heap profiles show allocation patterns that might exacerbate contention.
Explain what a spinlock is, how it differs from a blocking mutex, when spinlocks are a good choice (short critical sections, multi-core), and what risks they pose on single-core systems or in preemptible environments.
Sample Answer
A spinlock is a lightweight synchronization primitive where a thread repeatedly polls (spins) on a shared flag until it acquires the lock. It’s a busy-wait, non-blocking wait: the waiter consumes CPU cycles checking the lock rather than yielding the CPU.
How it differs from a blocking mutex:
- Spinlock: busy-waits on the CPU; typically implemented with atomic test-and-set or compare-and-swap. Low overhead to acquire/release when contention is brief.
- Blocking mutex: the waiter is suspended (kernel schedules another thread) and resumed later; involves context switch and scheduler overhead.
When spinlocks are a good choice:
- Very short critical sections (microseconds): spin avoids the expensive context switch of blocking.
- Low contention and predictable hold time.
- Multi-core systems where another core can make progress and release the lock quickly.
- Real-time or low-latency code where sleep/wake latency is unacceptable.
Risks on single-core or preemptible environments:
- Single-core: if the lock holder and waiter run on the same core, the waiter’s spinning prevents the holder from running and releasing the lock — deadlock-like starvation.
- Preemptible kernels/threads: a thread holding a spinlock can be preempted; other CPUs will spin forever until the holder is scheduled again → wasted CPU and increased tail latency.
- Power/thermal cost: spinning wastes cycles and energy.
- Priority inversion: a low-priority holder preempted while high-priority waiters spin wastes resources.
Best practices for SREs:
- Use spinlocks only for very short, uncontended critical sections on multi-core, non-preemptible contexts (or use adaptive spinlocks that spin then block).
- Prefer blocking mutexes for longer-held locks or user-space code, or use hybrid/adaptive designs and instrument lock hold times to decide.
Unlock Full Question Bank
Get access to all Concurrency, Synchronization & Deadlock interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.