Chapter 6

Concurrency

Concurrency in Nex is explicit and structured. A program does not manipulate threads or locks directly. It starts lightweight tasks and lets them communicate over typed channels. The model descends from Hoare’s Communicating Sequential Processes by way of the goroutine-and-channel discipline of Go.

This chapter extends the dynamic semantics of Chapter 5 to several tasks evaluating at once. Each task evaluates its own expressions and statements by the rules of Chapter 5, and all tasks share one store (Section 5.2). The rule that evaluation is sequential holds within each task, not across them.

The static typing of the concurrent forms is unremarkable, and was already given by the grammar of Chapter 2 and the classes of Appendix B.5. The interest here is all in their run-time behaviour.

6.1Tasks

The expression spawn do block end starts a new task that executes block concurrently with the task that created it. It immediately yields a task handle: a reference to a new object of class Task, or of class Task[T] when the block assigns a result of type T. The creator does not wait; the two tasks proceed in parallel. A rescue clause written before the closing end handles an exception raised by the block within the new task, as the handler of a scoped block does (Section 5.7).

The new task runs with the bindings in force at the spawn, so it can read the variables and reach the objects that were in scope where it was created. Because the store is shared, a task and its creator can see each other’s changes to objects. The language provides channels precisely so that such sharing need not be the means of communication.

A task handle supports the methods of the Task class (Appendix B.5).

let t: Task[Integer] := spawn do
  sleep(100)
  result := 7
end
print(t.is_done)
print(t.await)
print(t.is_done)

The spawn returns at once, so the first print shows false. await then waits for the task and prints 7, after which is_done is true.

A task that waits makes no progress until whatever it waits for happens. This Definition does not prescribe how tasks are scheduled. It requires only that a task which is able to make progress is not denied it for ever.

6.2Channels

A channel is a typed conduit between tasks: an object of class Channel[T], which carries values of type T.

Two sequence diagrams. On an unbuffered channel the sender waits at send until the receiver reaches receive, and the value passes directly between them. On a buffered channel of capacity 2 the first two sends complete at once, and the third waits until the receiver takes a value. Unbuffered sender receiver send(99) waits receive 99 continues yields 99 Buffered, capacity 2 sender channel receiver send(1) [1] send(2) [1, 2] send(3) waits receive: 1 [2] 3 [2, 3]
Figure 6.1. On an unbuffered channel, the sender waits at send until the receiver reaches receive, and the value passes directly between them. On a buffered channel, a send waits only while the buffer is full. The channel’s contents are shown in brackets.
let r: Channel[Integer] := create Channel[Integer]
let sender: Task := spawn do
  r.send(99)
  print("sender resumed")
end
sleep(100)
print("receiving")
print(r.receive)
sender.await

The sender reaches send first and waits there. Only when the main task reaches receive do the two meet. The program prints "receiving", then 99, then "sender resumed".

Each operation also has variants that do not wait, or wait only for a limited time.

A channel may be closed with close. After that:

The methods is_closed, size and capacity report a channel’s state. An unbuffered channel has capacity 0.

6.3Selective Communication

The select statement waits on several communications at once, and proceeds with whichever becomes ready first. Each clause names a channel operation or a task’s completion. An optional timeout clause gives up after an interval, and an optional else clause makes the select not wait at all.

select
  when jobs.receive as job then ...
  when results.send(value) then ...
  when worker.await as value then ...
  timeout 1000 then ...
  else ...
end

A clause is ready when its operation could complete at once:

The statement takes a ready clause, performs its operation, binds the clause’s as variable to the value obtained, and runs the clause’s block. Which clause it takes depends on what is ready:

An await clause is ready only on completion A when t.await clause never waits on its own account: it is ready only once t has finished, and then yields t’s result at once. With an else clause, it is therefore a test of whether the task has finished. Without else or timeout, the select as a whole waits, and the clause is taken when the task finishes, unless another clause becomes ready first.

6.4The Model and Its Realisations

The semantics above is given in the style of CSP, in which communicating tasks meet at a channel, and it is the authoritative meaning of the concurrent forms. Each back end realises it in a different way, and a programmer should know where the realisations differ.

In both realisations, a well-synchronised program behaves as the semantics above says. A well-synchronised program is one whose tasks communicate through channels rather than through shared mutable objects.

Programs whose tasks instead race to read and change shared objects are outside the guarantees of this chapter, and this Definition gives them no determinate meaning.