Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.