Deadlock freedom 2026-10-14
Absence of a reachable deadlock. In the finite acquisition model under deadlock, reaching a nonterminal state with no enabled operation is a finite witness to failure, so this formulation is a safety property. Sometimes the phrase is used for eventual acquisition by some requester, a liveness property; the intended requirement must be stated. Neither formulation by itself promises starvation freedom for every requester.
Lock ordering 2026-10-14
A shared order governing acquisition of distinct mutex objects. To prevent circular waiting, choose a strict total order and acquire only a higher-ranked mutex while retaining lower-ranked ones. In the two-object deadlock example, every participant acquires A before B. An outline of the argument is that ranks would strictly increase around any alleged waiting cycle, yet return to the starting rank, a contradiction. The rule must include acquisitions hidden inside called routines. It excludes this kind of deadlock, not arbitrary waiting on other resources or failure to schedule an owner.[ref]
A directed graph needs an explicit termination argument. After assigning its parent in a rooted tree, a vertex must still receive later traffic: having no new graph neighbours to discover is not proof that other senders have finished. Use a discovery wave together with duplicate-suppressed announcements of activated vertices. Both kinds of message identify their immediate sender, who has already activated.
sealed trait Msg { def from: NodeId }
final case class Discover(from: NodeId) extends Msg
final case class Active(from: NodeId, origin: NodeId) extends Msg
Let adj = neighbours.distinct. The actual communication objects must expose both input and output ports: the paper's Seq[![Msg]] annotation supplies only output capabilities, although the following sentence also requires receiving. Set up one shared-writer, single-reader buffered message-passing channel per vertex, publish its output in toNode, and keep its input in fromNode. Only node i reads fromNode(i). These are the two ports of the same underlying mailbox, not additional graph edges. The question's prohibition on buffered message-passing channels applies to Question 1(a), not to this question.
Take , reliable delivery, eventual scheduling, finite adj, and mailbox capacity . Every node has at most distinct incoming neighbours, including itself if there is a self-loop. The bound below ensures that no send blocks merely because a mailbox's receiver is not currently reading. Do not close these shared mailboxes on local process termination: late, redundant traffic is harmless and bounded. Their creator can release them after the parallel composition has joined. For example, the concrete setup can use
val mailbox = Vector.fill(N)(N2NBuf[Msg](B, 0, 1))
val toNode: Seq[![Msg]] = mailbox
val fromNode: Seq[?[Msg]] = mailbox
Here the zero writer count makes the shared output independent of individual writer departures; processes do not call its closeOut during the search.
def span: PROC = proc {
  val adj = neighbours.distinct
  var started = false
  val known = scala.collection.mutable.Set[NodeId]()

  def announce(origin: NodeId): Unit = {
    if (!known(origin)) {
      known += origin
      for (w <- adj) toNode(w) ! Active(me, origin)
    }
  }
  def activate(sender: NodeId): Unit = {
    started = true
    parent = if (me == 0) 0 else sender
    for (w <- adj if me == 0 || w != parent)
      toNode(w) ! Discover(me)
    announce(me)
  }

  if (me == 0) activate(0)
  while (known.size < N) {
    val message = fromNode(me) ? ()
    if (!started) activate(message.from)
    message match {
      case Discover(_) => ()
      case Active(_, origin) => announce(origin)
    }
  }
}
An announcement may be a vertex's first incoming message. It then serves as a discovery message too: its immediate sender is already active, and the receiving vertex adopts that sender as its parent in a rooted tree before forwarding anything. This preserves the required first-message rule. In particular, an inactive vertex never forwards announcements while remaining inactive, which could invalidate the parent-order argument.
For each nonroot vertex, its parent in a rooted tree activated strictly earlier. Repeated parent-following therefore cannot make a graph cycle and eventually reaches the unique initially active vertex, zero. Each parent assignment uses an actual incoming directed edge. Strong connectivity ensures eventual activation of every vertex: if an activated set were proper, a directed path from zero to an inactive vertex would have a first edge leaving that set. Its sender sends a discovery message along that edge; excluding its own parent does not exclude an inactive neighbour.
Each activation sends at most one Discover on each outgoing directed edge. For each of the origins, each vertex forwards Active at most once on each outgoing directed edge. Therefore a mailbox receives at most messages over the whole run, proving the chosen nonblocking storage bound. Each origin's announcement eventually reaches every vertex, since every first receipt forwards it on all outgoing directed edges. Thus every known set reaches size and every process terminates, even if redundant messages remain unread:
No timeout is used to conclude that the distributed work has finished. This uses larger finite mailboxes to make the termination proof simple, rather than assuming that simultaneous synchronous broadcasts cannot deadlock.
Run two dfFringe producers on synchronous one-to-one message-passing channels, and compare one received leaf from each. Catch closure separately on each input: otherwise the first closure could hide an extra value in the other fringe. An extra value or unequal pair is enough to stop immediately.
def sameFringe(node1: Array[Node], root1: NodeId,
               node2: Array[Node], root2: NodeId): Boolean = {
  val a = OneOne[NodeId]
  val b = OneOne[NodeId]
  var equal = true

  def next(p: ?[NodeId]): Option[NodeId] =
    try { Some(p ? ()) }
    catch { case _: io.threadcso.process.Stopped => None }

  val compare = proc {
    var done = false
    try {
      while (!done) {
        val x = next(a)
        val y = next(b)
        (x, y) match {
          case (None, None) => done = true
          case (Some(u), Some(v)) if u == v => ()
          case _ => equal = false; done = true
        }
      }
    } finally { a.closeIn; b.closeIn }
  }
  (dfFringe(node1, root1, a) || dfFringe(node2, root2, b) || compare)()
  equal
}
After successful comparisons, the first identities in the two leaf sequences agree. The first differing pair disproves equality of sequences; simultaneous exhaustion proves it; exhaustion on exactly one side disproves it. Therefore,
Closing both inputs on every comparator exit releases either producer if it is waiting in an output. The closure handler in dfFringe then returns normally, and the parallel composition joins before equal is read. For finite trees and eventual scheduling, the comparator's next read either obtains a leaf or observes completion, so there is no deadlock caused by reading the two inputs in a fixed order.
Only a constant amount of leaf data is stored. After an early mismatch, each producer may already have traversed toward its next leaf and be blocked offering it; cancellation prevents unbounded speculation or deliberate construction of the remaining fringe. Occasionally a short unequal fringe is already completely traversed when its final distinguishing value is compared. The streaming guarantee is that neither complete fringe needs to be materialized in advance, and work stops as soon as inequality is established.
Represent a binary semaphore by a two-element list containing the addresses of its busy flag and its waiting FIFO queue. A blocked continuation belongs to the waiting FIFO queue, not the scheduler's runnable FIFO queue. These definitions extend the FunMem scheduler from the preceding part:
val semaphore() =
  let val busy = new() in
  let val waiting = new() in
    busy := false; waiting := nil; busy : waiting : nil;

val acquire(s) =
  let val busy = head(s) in
  let val waiting = head(tail(s)) in
    if !busy then
      callcc(lambda (blocked)
        waiting := append(!waiting,blocked:nil); dispatch())
    else (busy := true; nil);

val release(s) =
  let val busy = head(s) in
  let val waiting = head(tail(s)) in
    if !waiting = nil then (busy := false; nil)
    else let val next = head(!waiting) in
      waiting := tail(!waiting); enqueue(next); nil
A newly created semaphore is free. Acquiring it while free marks it busy and immediately returns nil. Acquiring it while busy captures the remainder of the caller, appends that continuation to the waiting FIFO queue, and dispatches a runnable computation. No runnable copy of the blocked caller is retained.
When a waiting caller exists, release keeps the busy flag true: the released permit is reserved for that caller. It removes exactly one waiting continuation and appends it to the scheduler's runnable FIFO queue. It does not invoke dispatch, so the releasing computation keeps running. A newcomer cannot steal the reserved permit during the interval before the released caller resumes. That caller's captured continuation resumes after acquire with nil, already possessing the permit.
These are FIFO semaphores; the question only requires selecting one waiter, so choosing the oldest is a valid stronger discipline. The updates contain no suspension except at the explicit blocking point, making them atomic relative to this cooperative multitasking scheduler. As allowed in the paper, invalid releases and executions in which every remaining computation is blocked need no error handling; the latter are deadlocks, not a way to make progress.
With a synchronous reply, send(client,&answer) blocks until the client receives. An indefinitely unready client therefore stalls the smoothing operating-system process, which may in turn stall the timer service and its other clients as in the preceding part. If the client is merely delayed, this is blocking and delayed service; a deadlock requires a circular wait or a client that will never receive.
Requiring correctly used synchronous send-and-receive helps. A client calling sendrec(SMOOTH,&m) is committed to waiting for the matching reply as soon as its request has been accepted. This closes the scheduling window between a separate send and receive and prevents unrelated client computation from postponing that receive. It does not by itself fix slow measurements, arbitrary failures, circular dependencies between other services, or lack of CPU scheduling. With buffered replies the initial effect may instead be queue growth, depending on the message passing contract.
A system call that sends a request and commits the caller to receiving the matching reply as one coordinated operation, often named sendrec. It avoids the gap in which a client has completed a send but has not yet entered a receive. It does not make the service nonblocking or eliminate deadlock caused by other circular dependencies.