From dt-brigid
Concurrency model decision framework — CSP vs actors vs async/await vs structured concurrency, by problem characteristics and language
How this skill is triggered — by the user, by Claude, or both
Slash command
/dt-brigid:concurrency-model-selectionThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill provides a decision framework for choosing the right concurrency model given problem characteristics, language constraints, and operational requirements. It is not an encyclopedia — it focuses on the decision boundaries where picking the wrong model causes real pain.
This skill provides a decision framework for choosing the right concurrency model given problem characteristics, language constraints, and operational requirements. It is not an encyclopedia — it focuses on the decision boundaries where picking the wrong model causes real pain.
Sequential processes that communicate exclusively through typed channels. No shared memory. Composition through channel wiring.
Core primitives: channels (buffered/unbuffered), select/alt, goroutines or similar lightweight processes.
Strengths:
Weaknesses:
Where it lives: Go (native), Clojure core.async, Crystal, Kotlin channels, Rust crossbeam-channel.
Isolated processes with private state, communicating through asynchronous message passing. Each actor processes one message at a time.
Core primitives: actors/processes, mailboxes, supervision trees, location transparency.
Strengths:
Weaknesses:
Where it lives: Erlang/OTP (native), Akka (JVM), Microsoft Orleans (.NET), Elixir, Pony.
Suspendable computations managed by an event loop or executor. The programmer writes sequential-looking code that yields at I/O boundaries.
Core primitives: futures/promises, async functions, executors/runtimes, cancellation tokens.
Strengths:
Weaknesses:
Pin<Box<dyn Future>>) add complexity in systems languagesWhere it lives: JavaScript/TypeScript (native), Python asyncio, C# Task/ValueTask, Rust async (tokio/async-std), Kotlin coroutines, Swift concurrency.
Child tasks are scope-bound to their parent. When a scope exits, all child tasks are joined or cancelled. No fire-and-forget.
Core primitives: task groups, nurseries/scopes, cancellation scopes, error propagation.
Strengths:
Weaknesses:
Where it lives: Java 21+ (virtual threads + StructuredTaskScope), Kotlin coroutineScope, Swift TaskGroup, Python trio/anyio, Rust async (emerging patterns).
The same operation applied to many data elements simultaneously. Exploits hardware parallelism (SIMD, GPU, multi-core) with minimal coordination.
Core primitives: parallel iterators, SIMD intrinsics, GPU kernels, vectorized operations.
Strengths:
Weaknesses:
Where it lives: Rust Rayon, Java parallel streams, .NET PLINQ, Python multiprocessing/NumPy, CUDA/Vulkan compute, Go errgroup (coarse-grained).
| Problem Characteristic | Best Fit | Avoid |
|---|---|---|
| I/O-bound, many connections (HTTP servers, proxies) | Async/await | Data parallelism |
| CPU-bound, independent chunks (image processing, batch transforms) | Data parallelism | Actors (overhead) |
| Stateful entities with identity (user sessions, game entities, IoT devices) | Actors | CSP (no natural entity mapping) |
| Pipeline with stages (ETL, stream processing) | CSP | Actors (over-engineered) |
| Fault tolerance is primary concern (telecom, payment processing) | Actors (with supervision) | Raw async/await |
| Short-lived concurrent subtasks (parallel API calls, scatter-gather) | Structured concurrency | Actors (lifecycle overhead) |
| Mixed I/O and CPU (web app with compute-heavy endpoints) | Async/await + data parallelism hybrid | Single model forced everywhere |
| Shared mutable state, high contention (concurrent caches, counters) | Locks/atomics directly | CSP (serialization bottleneck) |
| Request/response with timeouts (microservice calls) | Async/await + structured concurrency | Actors (ask pattern complexity) |
| Long-running background workflows (batch jobs, cron-like) | CSP or actors | Structured concurrency (scope mismatch) |
Primary model: CSP (goroutines + channels). It is the language's native concurrency model and the ecosystem is built around it.
When to reach beyond CSP:
sync.Mutex, sync.Map, or atomic directly rather than serializing through a channel.errgroup for coarse-grained; for fine-grained, consider calling into C/Rust via CGo or using specialized libraries.errgroup.Group with context cancellation provides scope-bound task management.Primary model: Async/await (tokio) for I/O-heavy; Rayon for CPU-heavy. The ownership system makes shared-state concurrency safer than in other languages.
When to reach beyond async/await:
tokio::spawn_blocking (which wastes the async runtime's blocking pool).actix exists but the actor pattern is less natural in Rust due to ownership constraints on message types. Consider channels (crossbeam, tokio::mpsc) instead.tokio::JoinSet provides scope-bound task management.Primary model: Async/await with Task/ValueTask. The runtime, frameworks, and ecosystem assume this model.
When to reach beyond async/await:
Parallel.ForEachAsync, PLINQ, or System.Numerics.Vector<T> for SIMD.Task.WhenAll + CancellationTokenSource linking.Primary model: asyncio for I/O-bound; multiprocessing for CPU-bound (GIL bypass). The GIL means threading is not useful for CPU parallelism.
When to reach beyond asyncio:
multiprocessing, concurrent.futures.ProcessPoolExecutor, or NumPy/Pandas vectorized operations.trio or anyio for scope-bound task management. Prefer these over raw asyncio.gather.Primary model: Async/await with Promises. Single-threaded event loop; concurrency is cooperative by design.
When to reach beyond async/await:
Promise.allSettled + AbortController provides partial structured semantics. No native scope-bound tasks.Primary model: Virtual threads (Loom) + structured concurrency for new code. Thread-per-request is viable again with virtual threads.
When to reach beyond virtual threads:
ForkJoinPool for recursive decomposition.Primary model: Cooperative multitasking via signals and await. Godot's scene tree is single-threaded by default.
When to reach beyond signals/await:
WorkerThreadPool for physics, pathfinding, terrain generation. Keep rendering on the main thread.| Model | Typical Bug | Mitigation |
|---|---|---|
| CSP | Goroutine leak (blocked on channel nobody reads) | Context cancellation, bounded channels, leak detectors (goleak) |
| Actors | Mailbox overflow under load | Back-pressure protocols, bounded mailboxes, load shedding |
| Async/await | Executor starvation from blocking call | Dedicated blocking thread pool, lint rules against sync I/O in async |
| Structured concurrency | Scope too narrow (task cancelled prematurely) | Design scopes around logical operations, not syntactic blocks |
| Data parallelism | False sharing on cache lines | Padding, per-thread accumulators, reduction patterns |
The five models above are not exhaustive. Sometimes the right answer is a mutex, a read-write lock, or an atomic counter:
Mutex<HashMap> in Rust or sync.RWMutex in Go is simpler and faster than a channel or actor for a config cache read 1000x per second and written once per minute.AtomicU64, atomic.Int64) for metrics, feature flags, reference counts. No model overhead needed.cross-language-interop, roslyn-analyzers for .NET, rust-project-patterns) for implementation details beyond concurrency.npx claudepluginhub dreamteam-hq/brigid --plugin dt-brigidGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.