Concurrency
SSPUR has structured concurrency only: tasks start inside a scope and all of them finish before the scope does, so a task can’t outlive the code that started it.
par
par(a, b, ...) runs each argument as a task and returns the tuple of results:
fn slow_square(n: Int) -> Int
= (0..n).map(i => n).sum
fn both() -> (Int, Int)
= par(slow_square(300), slow_square(400))
for x in par(xs) runs the loop body once per element, each as a task, and waits for all of them. Tasks share only immutable values, atomics and channels.
Atomics
An Atomic[Int] is a counter that tasks can update together. atomic(v) makes one, with .load, .store(v), .add(d) (which traps on overflow) and .cas(expected, new). Using atomics or channels is the conc effect.
fn count_multiples(n: Int, k: Int) -> Int ! conc
= do
hits = atomic(0)
for w in par(0..4)
hits.add((w * n / 4..(w + 1) * n / 4).filter(_ % k == 0).len)
hits.load
Channels
chan() makes an unbounded channel with .send(x), .recv (an Opt, none once closed and empty) and .close. for x in c reads until the channel is closed and empty:
fn produce(out: Chan[Int], n: Int) -> Unit ! conc
= do
for i in 1..n + 1
out.send(i)
out.close
fn consume(c: Chan[Int]) -> Int ! conc
= do
var s = 0
for x in c
s := s + x
s
fn pipeline(n: Int) -> Int ! conc
= do
c = chan()
(_, total) = par(produce(c, n), consume(c))
total
$ sspur test concurrency.ssp
3 passed, 0 failed
If every task is blocked in recv, the program stops with deadlock: every task is blocked on recv instead of hanging.
What the checker rejects
Data races don’t compile. A task can’t assign a variable declared outside it:
fn count_multiples(n: Int, k: Int) -> Int
= do
var hits = 0
for w in par(0..4)
hits := hits + (w * n / 4..(w + 1) * n / 4).filter(_ % k == 0).len
hits
$ sspur check race.ssp
race.ssp:5:5 E_PAR_RACE task assigns 'hits', which is declared outside the task; tasks run concurrently, so this is a data race
hint: return the value from the task, or use an Atomic[Int] or a Chan
A task also can’t use a function value from outside it (E_PAR_SHARE; call top-level functions by name), perform log or a handled effect (E_PAR_EFFECT), or return (E_RETURN_IN_PAR). If tasks fail, the leftmost task’s error wins, after every task has finished.
Automatic parallelism
You often don’t need par at all. A pure map or filter pipeline over a large list is split across cores by the native compiler, because a pure lambda can’t observe the order it runs in. SSPUR_THREADS=1 turns that off. Native code runs each par task on its own thread; the interpreter runs one task at a time and switches when a task blocks, with the same results.
Next: Networking.