Concurrency Patterns
Description
Learn to implement robust concurrency with async/await, Promises, threads, and message passing, while avoiding race conditions and timing bugs.
When to Use
How do I implement concurrency patterns with async/await? | Help me debug race conditions in my code. | I need to know when to use Promise.all vs Promise.allSettled. | Show best practices for avoiding deadlocks and timing bugs. | I want to design thread-safe code using message passing.
Use Cases
Coordinate worker threads to process a shared queue | Debug timing-dependent bugs in async code | Parallelize CPU-bound tasks with safe sharing | Choose between Promise.all and Promise.allSettled for results
SKILL.md Content
---
name: concurrency-patterns
description: "Learn to implement robust concurrency with async/await, Promises, threads, and message passing, while avoiding race conditions and timing bugs."
metadata:
tags: "concurrency, async-await, promises, threads, race-conditions, deadlocks, debugging"
source: "https://skilldb.dev/skills/autonomous-agent-skills/concurrency-patterns"
pack: "autonomous-agent-skills"
category: "Autonomous Agents"
---
# Concurrency Patterns
## When to use this skill
Use when the user says things like:
- "How do I implement concurrency patterns with async/await?"
- "Help me debug race conditions in my code."
- "I need to know when to use Promise.all vs Promise.allSettled."
- "Show best practices for avoiding deadlocks and timing bugs."
- "I want to design thread-safe code using message passing."
You are an AI agent writing and debugging concurrent code. Your role is to implement correct concurrent patterns using async/await, Promises, threads, and message passing — while avoiding race conditions, deadlocks, and the subtle timing bugs that make concurrency difficult.
## Philosophy
Concurrency bugs are the hardest class of bugs because they are non-deterministic. Code that works 99% of the time can fail in production under load, and the failure may be unreproducible in development. The defense against this is not cleverness but simplicity: use the simplest concurrency model that solves the problem, minimize shared mutable state, and prefer well-tested primitives over custom synchronization logic. When you must share state, make the access patterns obvious and explicit.
## Techniques
### Async/Await Patterns
- Use `async/await` for I/O-bound operations: network requests, file system access, database queries. It allows the runtime to do other work while waiting.
- Always `await` async function calls. Forgetting `await` creates a dangling Promise that runs independently — errors will be swallowed and ordering will be wrong.
- Use `try/catch` around `await` calls for error handling. An unhandled rejection in an async function may crash the process or be silently ignored.
- Async functions always return Promises. Even if the function body is synchronous, wrapping it in `async` changes its return type.
- In loops, consider whether iterations should run sequentially (`for...of` with `await`) or concurrently (`Promise.all` with `map`).
### Promise Handling
- `Promise.all()` runs Promises concurrently and fails fast — if any Promise rejects, the whole result rejects. Use it when all results are needed and any failure is fatal.
- `Promise.allSettled()` runs Promises concurrently and waits for all to complete, regardless of success or failure. Use it when you need results from as many as possible.
- `Promise.race()` resolves with the first settled Promise. Use it for timeouts: race the operation against a timer.
- Never create Promises without handling rejection. Attach `.catch()` or use `try/catch` with `await`.
- Avoid mixing callback-style and Promise-style code. Convert callbacks to Promises with `util.promisify` (Node) or manual wrapping.
### Race Conditions
- A race condition occurs when the result depends on the timing of events that are not guaranteed to occur in a specific order.
- Check-then-act patterns are a classic source: checking if a file exists then writing to it — another process may create the file between the check and the write.
- In web applications, race conditions appear when multiple requests modify the same resource. Use optimistic locking (version numbers) or pessimistic locking (database locks).
- In frontend code, race conditions occur when a fast response arrives after a slow one — the UI shows stale data. Cancel or ignore outdated requests.
- Use atomic operations when available. Database transactions, `compareAndSet`, and atomic file writes prevent intermediate states.
### Deadlocks
- A deadlock occurs when two or more tasks wait for each other to release resources, and none can proceed.
- The classic pattern: Task A locks resource 1 and waits for resource 2. Task B locks resource 2 and waits for resource 1. Neither can proceed.
- Prevention: always acquire locks in a consistent global order. If every task locks resources in the same sequence, circular waits cannot form.
- Detection: if a system hangs without error, suspect a deadlock. Examine what each task is waiting for.
- Avoidance: prefer lock-free designs. Use message passing, immutable data, or single-writer patterns instead of multiple locks.
### Thread Safety
- Data accessed by multiple threads must be protected. Unprotected concurrent reads and writes produce corrupted data.
- In JavaScript (single-threaded event loop), race conditions come from interleaved async operations, not threads. But Web Workers and Worker Threads introduce true parallelism.
- In Go, use channels for communication between goroutines. The motto is "share memory by communicating, do not communicate by sharing memory."
- In Python, the Global Interpreter Lock (GIL) prevents true parallelism for CPU-bound code. Use `multiprocessing` for CPU parallelism, `asyncio` for I/O parallelism.
- In Rust, the borrow checker prevents data races at compile time. If it compiles, shared mutable access is safe.
- Use thread-safe data structures (`ConcurrentHashMap`, `sync.Map`, `Mutex<T>`) when sharing state is necessary.
### Worker Threads and Message Passing
- Offload CPU-intensive work to worker threads to keep the main thread responsive.
- Workers communicate via message passing (postMessage/onmessage in browsers, parentPort in Node workers). Data is copied or transferred, not shared.
- Use `SharedArrayBuffer` and `Atomics` only when message passing overhead is unacceptable and you understand the memory model.
- In Go, goroutines with channels provide lightweight concurrency with clean message passing semantics.
- Size worker pools appropriately: too few waste CPU, too many waste memory and increase context switching.
### Choosing Parallelism Strategies
- I/O-bound work (API calls, database queries, file reads): use async/await. One thread can handle many I/O operations concurrently.
- CPU-bound work (image processing, data transformation, compression): use worker threads or separate processes. Async does not help because the CPU is the bottleneck.
- Mixed workloads: use async for I/O and offload CPU work to workers.
- Consider whether operations need to be concurrent at all. Sequential code is easier to reason about and debug. Add concurrency only when performance requires it.
### Debugging Timing-Dependent Bugs
- Timing bugs are hard to reproduce because they depend on execution order. Adding logging can change timing and hide the bug.
- Use deterministic tests: mock timers, control scheduling, use barriers to force specific execution orders.
- Look for shared mutable state accessed without synchronization. This is the root cause of most concurrency bugs.
- Stress testing (running many concurrent operations) can surface race conditions that appear rarely under normal load.
- Thread sanitizers (TSan) and race detectors (Go's `-race` flag) can detect data races automatically.
## Best Practices
- Default to sequential code. Add concurrency only when there is a measured performance need.
- Use structured concurrency: every concurrent task should have a clear owner that waits for its completion and handles its errors.
- Cancel outdated work. When a new request supersedes an old one, cancel the old one to avoid wasted work and stale results.
- Set timeouts on all concurrent operations. Without timeouts, a hung operation can block the system indefinitely.
- Test concurrent code with many iterations and concurrent executions, not just single-threaded happy paths.
## Anti-Patterns
- **Fire and forget**: Starting async operations without awaiting or handling errors. Failures become silent, ordering becomes unpredictable.
- **Shared mutable state without protection**: The root of nearly all concurrency bugs. Protect shared state with locks, atomic operations, or immutable data.
- **Blanket parallelization**: Making everything concurrent without measuring adds complexity without proportional benefit. Profile first.
- **Nested locks**: Acquiring locks inside other locks invites deadlocks. Keep lock scopes narrow and avoid nesting.
- **Busy waiting**: Spinning in a loop checking a condition wastes CPU. Use event-based signaling (condition variables, Promises, channels).
- **Ignoring cancellation**: Long-running concurrent tasks that cannot be cancelled waste resources and block shutdown.
- **Assuming ordering without synchronization**: Two async operations started in sequence may complete in any order. Use explicit synchronization when ordering matters.