agentsclimarketplace

Crystal concurrency

Skill dsisnero/crystal_forge/skills/crystal-concurrency

A set of skills to help with Crystal development

Install
npx -y skills add dsisnero/crystal_forge --skill crystal-concurrency

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Crystal concurrency and parallelism patterns — fibers, channels, select, WaitGroup, ExecutionContext, and porting Go concurrency patterns. Use when implementing any concurrent or parallel Crystal code, debugging deadlocks or fiber leaks, choosing between spawn and ExecutionContext::Parallel, or translating Go channel patterns to Crystal. Covers 41 patterns across 6 categories, ported from Go and verified against an upstream spec suite, with runnable examples and measured parallel benchmarks (up to 8.76x speedup).

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

13.5 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it

Crystal Concurrency Patterns

Use this skill when implementing concurrency in Crystal — fibers, channels, select, WaitGroup, parallel execution contexts, or porting Go concurrency patterns. Also use when debugging deadlocks, fiber leaks, or MT-safety issues in Crystal code.

If the user asks whether the concurrency is actually faster, wants throughput numbers, worker-count comparisons, hotspot validation, or says not to guess, also use crystal-benchmarking.

Core Rules

These rules prevent the most common bugs. Violating any of them causes silent deadlocks, data races, or ambiguous behavior.

  1. Use Channel(Nil) only with receive; use a non-nil type with receive?Channel(Nil) is a fine one-shot completion signal when the receiver calls receive. But receive? uses nil as its "closed" sentinel, so a Channel(Nil) cannot distinguish "got a value" from "channel closed". Use Channel(Bool) (or another non-nil type) for close-aware done, quit, and semaphore channels.

  2. receive? in select for close-safe receivesreceive raises ClosedError when a channel closes inside a select. Use receive? which returns nil instead.

  3. done.close for broadcast cancellation — closing a channel wakes ALL fibers waiting on receive?. This is how you cancel an unknown number of workers. Send a value to cancel one; close to cancel all.

  4. select ... else ... end = Go's select { default: } — the else branch fires when no channel operation can complete immediately (non-blocking).

  5. Merge closeable fan-out outputs — bare select when ch.receive across multiple channels that may close raises ClosedError. Merge outputs with WaitGroup + receive?, especially when the outputs can run in parallel.

  6. Double-close is safe — Crystal silently ignores closing an already-closed channel. Go panics. Don't rely on this.

  7. WaitGroup is built-inrequire "wait_group". Has add, done, wait, and spawn methods. Direct equivalent of Go's sync.WaitGroup.

Go-to-Crystal Translation

GoCrystal
go func()spawn { }
chan TChannel(T)
make(chan T, n)Channel(T).new(n)
<-chch.receive
ch <- vch.send(v)
close(ch)ch.close
for v := range chwhile v = ch.receive?
select { case ... }select when ... end
select { default: }select ... else ... end
sync.WaitGroupWaitGroup
time.After(d)helper: spawn + sleep + channel send
context.WithCancelChannel(Bool) + done.close

Pattern Index

41 patterns ported from dsisnero/crystal-concurrency-patterns (the Crystal port of lotusirous' Go Concurrency Patterns) and split across six category reference files. Open the file for the category you need — each entry has fuller, self-contained Crystal code, the gotcha that bites people, and a citation to the upstream spec (characterized) or src/example (demonstrated) it came from. The self-contained code blocks type-check under Crystal 1.20.2 (the Subscription entry is an annotated sketch of the nil-channel workaround, not a standalone program).

Patterns marked with an example file have a complete runnable program in examples/ showing the full lifecycle (producer → channel → workers → WaitGroup → close).

Basic — references/basic.md

PatternWhat it doesExample
GeneratorFiber + channel; the channel is the stream
Fan-InMerge N input channels into one (WaitGroup close)
Fan-OutN workers compete on one source; each value goes to one
PipelineChain stages, each closing its outbound channelpipeline_cancel.cr
ConfinementOne fiber owns the data; publish via channel
For-Select LoopLong-lived fiber polling done with select/else
Repeat / TakeComposable infinite generator bounded by take
Error-Handling ChannelCarry value-or-error so a stage never crashes

Coordination — references/coordination.md

PatternWhat it doesExample
Worker PoolFixed workers pull jobs, WaitGroup closes resultsworker_pool.cr
Bounded ParallelismFixed pool walks a tree with done cancellationparallel_digest.cr
Queuing (Semaphore)Buffered Channel(Bool) caps concurrency
Daisy ChainN fibers relay a token in a line
Restore SequencePer-message wait channel restores ordering
Ping-PongVolley one mutable object; ownership moves with send

Cancellation — references/cancellation.md

PatternWhat it doesExample
Done Channelclose broadcasts cancel to all waiterspipeline_cancel.cr
Quit SignalTwo-way stop: request + acknowledge
Or-ChannelMerge signals; fire when any input closes
Or-DoneWrap a value channel so reads respect done
ErrgroupCancel siblings on first error, return iterrgroup.cr
Graceful ShutdownOrdered teardown: done → jobs → wait
Select TimeoutBound a receive with select ... when timeout(span)
Contextdone + cancel proc = WithCancel / WithTimeout

Data Flow — references/data-flow.md

PatternWhat it doesExample
Tee ChannelDuplicate each value to two outputs (flag workaround)
Bridge ChannelFlatten a channel-of-channels into one stream
Ring BufferKeep last N, drop oldest (Deque, not select/else)
BroadcasterEvery subscriber gets every message
Pub/SubTopic-routed broadcaster with Mutex-guarded mappubsub.cr
SubscriptionRSS aggregator; the nil-channel-in-select workaround

Resilience — references/resilience.md

PatternWhat it doesExample
Rate LimitingOne op per fixed interval
Bursty Rate LimitingToken bucket allowing short bursts
Retry with BackoffExponential delays between attempts
Circuit BreakerFail fast after N failures; cool down; half-open
BackpressureBounded channel buffer is the flow control
Batch / DebounceFlush on size or on a quiet window

Computation — references/computation.md

PatternWhat it doesExample
Future / PromiseStart work now, collect later (cap-1 channel)
First ResponseRace replicas, take fastest (buffered, no leak)
Scatter-GatherFan out, gather until one shared deadline
Map-ReduceParallel map, sequential reduce
Stateful Fiber (Actor)One fiber owns state; access via request channelsactor.cr
Ticker with CancellationTick on interval until done
Mutex-Protected StateGuarded counter/map when an Actor is overkill

Execution Context Decision Tree

Read references/execution-contexts.md for code examples and benchmarks.

Is the work I/O-bound?
├── Yes → `spawn` in the current context; standard-library I/O yields to the
│        event loop while it waits.
└── No (CPU-bound or intentionally blocking)
    ├── Parallelizable CPU work? → `ExecutionContext::Parallel`
    │   ctx = Fiber::ExecutionContext::Parallel.new("name", maximum: capacity)
    │   ctx.spawn { work }
    ├── One task must own a thread for its lifetime? → `ExecutionContext::Isolated`
    │   main = Fiber::ExecutionContext::Isolated.new("name") { blocking_call }
    │   main.wait
    └── Need an independent, non-parallel group? → `ExecutionContext::Concurrent`
        (one runnable fiber at a time; a blocking fiber blocks this context)

Crystal 1.21 execution-context rules

Execution contexts are enabled by default in Crystal 1.21. The default context is Parallel, but its initial parallelism is 1 for backward compatibility. To make process-default work parallel, resize it explicitly; otherwise create and use a named Parallel context. Parallelism is a maximum capacity, not a promise of a fixed number of dedicated worker threads. A parallel context autoscales up to that capacity. Too many simultaneously blocking fibers can exhaust it and then block remaining work; bound blocking work with a semaphore or use another context.

default = Fiber::ExecutionContext.default
default.resize(Fiber::ExecutionContext.default_workers_count)

Outside an Isolated context, spawn uses the current fiber's execution context; ctx.spawn chooses another one. An isolated fiber cannot spawn another fiber in its own context: configure spawn_context: when creating it, or call a different context's spawn. A fiber never moves between contexts, but a fiber in a Parallel or Concurrent context is not pinned to an OS thread and can resume on a different thread. Avoid @[ThreadLocal] and do not retain thread-local assumptions across a yield or blocking call.

Do not jump to ExecutionContext because a path "looks parallelizable". Measure the current path first and identify whether the real cost is I/O, parser work, FFI, cache persistence, or actual CPU-bound computation.

Measured speedups (Apple Silicon arm64, 8 workers):

  • Map-reduce (CPU math): 3.4x with 4 threads
  • MD5 hashing (200 items): ~6x with 4 threads
  • File digest (1014 files): 8.76x with 8 threads
  • Mixed I/O+CPU (2154 files): 4.42x with 8 threads

MT Safety Checklist

In Crystal 1.21, any Parallel context (including a resized default context) means shared state may be accessed concurrently:

  • Channels are thread-safe by design
  • WaitGroup is implemented with atomics
  • Mutex#synchronize is fiber-safe
  • Atomic maps to hardware atomics
  • Actor pattern (single fiber owns state) is naturally safe
  • Bare select with receive on closeable channels — use receive? or merge pattern
  • Shared mutable state without locks — add Mutex or Atomic
  • Timing-sensitive assertions — add tolerance, thread scheduling is non-deterministic
  • Thread-local assumptions across yields — invalid in Parallel; a fiber can resume on a different OS thread. Concurrent fibers can also switch threads after a blocking syscall.

Scheduling and Lifetime Rules

  • Fibers are cooperative. CPU-bound code that neither blocks nor calls Fiber.yield monopolizes its scheduler; yield deliberately in long-running cooperative work.
  • spawn queues work; it does not run the fiber immediately. The process exits when the main fiber completes, so join work with a Channel or WaitGroup instead of using sleep as completion synchronization.
  • Prefer spawn method_call(argument) when a loop-local value changes between iterations: the spawn macro captures the call arguments. A bare spawned block captures an outer local by reference; block parameters are safe.
  • A buffered channel controls backpressure and scheduling, not worker lifetime. A send blocks only when no receiver is already waiting and the buffer is full.

References

  • references/channel-rules.md — closed channels, nil channels, Channel(Nil) ambiguity, receive vs receive?, MT behavior findings with before/after code
  • Pattern reference files (full code + gotchas + source citations), one per category in the index above: references/basic.md, references/coordination.md, references/cancellation.md, references/data-flow.md, references/resilience.md, references/computation.md
  • references/execution-contexts.md — Parallel, Concurrent, Isolated with benchmarks, worker pool and map-reduce examples
  • Crystal 1.21 official documentation: Concurrency guide, Parallelism guide, ExecutionContext API, and 1.21 release notes

Full Examples

Read these when implementing a complex pattern — they show complete wiring (producer → channel → workers → WaitGroup → results → close lifecycle):

  • examples/worker_pool.cr — producer→jobs→workers→results with spawn vs ctx.spawn benchmark. The template for any worker pool.
  • examples/parallel_digest.cr — walk directory, hash files, benchmark default vs ExecutionContext::Parallel. Real-world bounded parallelism.
  • examples/actor.cr — stateful fiber with read/write request channels, concurrent readers and writers, clean shutdown.
  • examples/errgroup.cr — run N tasks, cancel all on first error via done.close, capture first exception.
  • examples/pipeline_cancel.cr — gen→square→filter pipeline with done channel through every stage, fan-out/fan-in with merge, early consumer exit.
  • examples/pubsub.cr — PubSub class with subscribe/unsubscribe/publish, Mutex-protected subscriber map, topic routing, clean shutdown.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.