klain:sync — Go-style goroutines & channels
The concurrency differentiator, and the one place the project aims for full Go fidelity: go(fn) to spawn a goroutine, a typed Channel<T> to communicate, select to wait on many at once — all running in parallel across every core, preemptively scheduled, in a single native binary. This is the deliberate answer to "why not just Workers or async?"
A concurrent program, end to end
Here is a real fan-out / fan-in pipeline: it counts the primes below half a million across a pool of goroutines, streams each hit back over a channel, and coordinates completion with select — the same program you'd write in Go.
import { go, Channel, select, defaultCase } from 'klain:sync'
const N = 500000
const WORKERS = 8
const primes = new Channel<number>(1024) // every prime found, streamed to the collector
const done = new Channel<number>(0) // a worker signals when its slice is exhausted
function isPrime(n: number): boolean {
if (n < 2) return false
if (n % 2 === 0) return n === 2
for (let d = 3; d * d <= n; d += 2) {
if (n % d === 0) return false
}
return true
}
// Fan out: worker `id` tests the interleaved slice id, id+WORKERS, id+2·WORKERS…
function spawnWorker(id: number): void {
go(() => {
for (let n = 2 + id; n <= N; n += WORKERS) {
if (isPrime(n)) primes.send(n)
}
done.send(id)
})
}
for (let w = 0; w < WORKERS; w++) spawnWorker(w)
// Fan in: count primes as they stream in, tally completions with select, then
// drain any still-buffered primes with a non-blocking default.
let count = 0
let finished = 0
while (finished < WORKERS) {
select(
primes.recvCase((p: number) => { count += 1 }),
done.recvCase((id: number) => { finished += 1 }),
)
}
let draining = true
while (draining) {
select(
primes.recvCase((p: number) => { count += 1 }),
defaultCase(() => { draining = false }),
)
}
console.log(`primes below ${N}: ${count}`)$ klainmain build parallel_primes.ts && ./parallel_primes
primes below 500000: 41538
(found by 8 goroutines running in parallel)Goroutines & the scheduler
go(() => { … }) spawns a goroutine — far cheaper than an OS thread — onto a real GMP work-stealing scheduler built into an embedded C runtime that is linked only when you import klain:sync (a program that never imports it pays and links nothing).
- G — a goroutine: its own stack and saved execution context.
- M — an OS thread (
pthread). - P — a logical processor with a local run queue;
GOMAXPROCSdefaults to the CPU count, so N runnable goroutines run on N cores — real parallelism, unlikeasync(single-threaded by contract).
Idle processors steal half of a busy one's queue, and a global run queue backstops them, so work spreads evenly with no central bottleneck.
Channels
A Channel<T> is a Go channel: a synchronous, blockingsend/receive that parks the goroutine — the OS thread keeps running other goroutines — rather than blocking a whole thread. Capacity 0 is an unbuffered rendezvous; a positive capacity is a buffered ring.
const ch = new Channel<number>(0) // unbuffered rendezvous
go(() => { ch.send(42) }) // parks the goroutine until a receiver arrives
console.log(ch.receive()) // 42
// Range a channel until it is closed and drained:
const nums = new Channel<number>(0)
go(() => {
for (let i = 1; i <= 5; i++) nums.send(i * i)
nums.close()
})
let sum = 0
for (const v of nums) sum += v // 1 + 4 + 9 + 16 + 25
console.log(sum) // 55 Ranging a channel with for (const v of ch) receives until the channel is close()d and drained — the idiomatic way to consume a stream of values.
select
select waits on several channel operations at once and proceeds with whichever is ready; if several are ready it picks pseudo-randomly (Go's fairness), and if none are ready it blocks the goroutine until one fires — unless a defaultCase makes it non-blocking.
const a = new Channel<number>(0)
const b = new Channel<number>(0)
go(() => { a.send(1) })
go(() => { b.send(2) })
// Take whichever is ready first; if neither is, block until one fires.
select(
a.recvCase((v: number) => { console.log('from a:', v) }),
b.recvCase((v: number) => { console.log('from b:', v) }),
)
// With a default, select never blocks:
select(
a.recvCase((v: number) => { console.log('got', v) }),
defaultCase(() => { console.log('nothing ready') }),
)Preemptive by default
A busy goroutine can't starve the others. A sysmon helper thread flags any goroutine that has run too long, and the compiler inserts a tiny preempt check at every function entry and every loop back-edge — so even a tight compute loop with no channel operations yields. Because every loop is instrumented, cooperative preemption is complete here; there is no uninstrumented loop that Go's signal-based async preemption would need to catch.
When a goroutine makes a genuinely blocking call, work-stealing lets the other processors keep draining its queue, and if every thread blocks at once a bounded rescue thread is spawned so the program can't deadlock.
Tuning
GOMAXPROCS— number of logical processors (parallelism); defaults to the CPU count.KLAINSYNC_STACK_KB— per-goroutine stack size in KiB (default 256); lower it to pack far more goroutines, raise it for deep recursion.
Scope fence. None of this touches async/await, Promises, or Workers — those stay JS-faithful and single-threaded so ordinary code keeps running under Node. Goroutines are a separate, opt-in world reached only through klain:sync, and channel operations are synchronous (Go has no await on a channel). Only the moving/growable-stack optimisation is still ahead; it shares the precise-stack-map work with the moving-GC backend.