Zulip Chat Archive

Stream: general

Topic: formalizing multithreading


Frederick Pu (Oct 05 2025 at 19:27):

I've been procrasinating my operating systems homework so I decided to formalize multithreading

import Std

open Std

/-
  a monad `m` can be collected into a list of atomic instructions
  `β` is the instruction type
-/
class Instruction (m : Type  Type) [Monad m] (β : Type) where
  instructions {α} : m α  m (List β)

/--
`ThreadCollector m β σ`:

Represents a **scheduler** that tracks the evolution of a global scheduling state `σ`
as threads execute atomic instructions of type `β` in monad `m`.
-/
class ThreadCollector (m : Type  Type) (β σ : Type) where
  /--
  Update the scheduler after a thread executes an atomic instruction.

  - `sched` — the current scheduler state.
  - `instr` — the instruction the thread wants to execute.
  - `threadIdx` — index of the thread performing the instruction.

  Returns:
  * `none` if the thread is **denied** (e.g., tries to lock an already-locked mutex
    or is currently asleep).
  * `some mσ` if the thread is allowed to execute, producing a monadic action
    `m σ` that updates the scheduler state after the instruction.
  -/
  step : σ  β  (threadIdx : Nat)  Option (m σ)

/--
`scheduleOf sched instrs` means that `sched` is a **valid interleaving**
of the per-thread instruction lists `instrs`.

Formally:
1. Every instruction in `sched` is tagged by a valid thread index.
2. When we group `sched` by these thread tags, we recover exactly each `instrs[k]`.
3. `sched` and `tags` have the same length.
-/
def scheduleOf {β : Type} (sched : List β) (instrs : List (List β)) : Prop :=
   (tags : List Nat),
    -- (1) sched and tags have the same length
    sched.length = tags.length 
    -- (2) every tag is a valid thread index
    ( i, i < tags.length  tags[i]! < instrs.length) 
    -- (3) grouping by thread tag recovers the original instrs[k] (List.filterMap perserves order)
    ( k < instrs.length,
      let grouped := (sched.zip tags).filterMap (fun (b, t) =>
        if t = k then some b else none);
        grouped = instrs[k]!)

I was wondering if there were similar efforts or if anybody has feedback on my formalisms

Frederick Pu (Oct 05 2025 at 19:29):

my next goals are to formalize mutexs and show that if you have n threads each of which is locking from a list of m mutexs as long as all threads lock in ascending order of the mutex list then a deadlock can never happen

Frederick Pu (Oct 05 2025 at 19:45):

here's teh formalizm up to deadlocks

import Std

open Std

/-
  a monad `m` can be collected into a list of atomic instructions
  `β` is the instruction type
-/
class Instruction (m : Type  Type) [Monad m] (β : Type) where
  instructions {α} : m α  m (List β)

/--
`ThreadCollector m β σ`:

Represents a **scheduler** that tracks the evolution of a global scheduling state `σ`
as threads execute atomic instructions of type `β` in monad `m`.
-/
class ThreadCollector (m : Type  Type) (β σ : Type) where
  /--
  Update the scheduler after a thread executes an atomic instruction.

  - `sched` — the current scheduler state.
  - `instr` — the instruction the thread wants to execute.
  - `threadIdx` — index of the thread performing the instruction.

  Returns:
  * `none` if the thread is **denied** (e.g., tries to lock an already-locked mutex
    or is currently asleep).
  * `some mσ` if the thread is allowed to execute, producing a monadic action
    `m σ` that updates the scheduler state after the instruction.
  -/
  step : σ  β  (threadIdx : Nat)  Option (m σ)

/--
`scheduleOf sched instrs` means that `sched` is a **valid interleaving**
of the per-thread instruction lists `instrs`.

- `sched : List (Nat × β)` is a list of `(threadIdx, instruction)` pairs.
- `instrs : List (List β)` is the per-thread list of instructions.

Formally:
1. Every `(threadIdx, _)` in `sched` refers to a valid thread index.
2. For each thread `k`, collecting all instructions in `sched` with `threadIdx = k`
   (in order) recovers exactly `instrs[k]`.
-/
def scheduleOf {β : Type} (sched : List (Nat × β)) (instrs : List (List β)) : Prop :=
  -- (1) every tag is a valid thread index
  ( ib  sched, ib.1 < instrs.length) 
  -- (2) grouping by thread tag recovers the original instrs[k]
  ( k < instrs.length,
    let grouped := sched.filterMap (fun (t, b) =>
      if t = k then some b else none);
    grouped = instrs[k]!)

/--
`partialScheduleOf prefix instrs` means that `prefix` is a **valid partial interleaving**
of the per-thread instruction lists `instrs`.

- `prefix : List (Nat × β)` is a prefix of a schedule.
- `instrs : List (List β)` is the per-thread list of instructions.

Formally:
1. Every `(threadIdx, _)` in `prefix` refers to a valid thread index.
2. For each thread `k`, collecting all instructions in `prefix` with `threadIdx = k`
   (in order) forms a **prefix** of `instrs[k]`.
-/
def partialScheduleOf {β : Type} (pref : List (Nat × β)) (instrs : List (List β)) : Prop :=
  -- (1) every tag is a valid thread index
  ( ib  pref, ib.1 < instrs.length) 
  -- (2) grouping by thread tag is a prefix of each thread's instructions
  ( k < instrs.length,
    let grouped := pref.filterMap (fun (t, b) =>
      if t = k then some b else none);
    grouped.length  instrs[k]!.length 
    grouped = instrs[k]!.take grouped.length)

example {β : Type} (pref : List (Nat × β)) (instrs : List (List β)): partialScheduleOf pref instrs   sched, scheduleOf sched instrs  pref.length  sched.length  pref = sched.take pref.length := sorry

/--
`scheduleValid sched σ0 TC` means that executing `sched` from initial state `σ0`
using the `ThreadCollector` `TC` always produces `some σ` at every step.
-/
def scheduleValid {m : Type  Type}{β σ : Type} [Monad m]
  (TC : ThreadCollector m β σ)
  (sched : List (Nat × β))  -- (threadIdx, instr)
  (σ0 : σ) : Prop :=
   σFinal : σ,
    sched.foldlM (fun s (ti, instr) =>
      match TC.step s instr ti with
      | some ms => ms  -- bind the monadic action
      | none    => pure s  -- stop counting as valid? (could alternatively fail)
    ) σ0 = pure σFinal

/--
`deadlocks TC σ0 instrs pref` means that after executing the partial schedule `pref`,
the system is in **deadlock** under the `ThreadCollector` `TC` starting from state `σ0`.

Formally:
- `pref : List (Nat × β)` is a valid partial schedule (`partialScheduleOf pref instrs`).
- After `pref`, **any extension** of the remaining instructions cannot produce a
  full valid schedule where every step returns `some`.
-/
def deadlocks {m : Type  Type} {β σ : Type} [Monad m]
  (TC : ThreadCollector m β σ)
  (σ₀ : σ)
  (instrs : List (List β))
  (pref : List (Nat × β)) : Prop :=
  partialScheduleOf pref instrs 
   extension : List (Nat × β),
    -- extension must respect per-thread order of remaining instructions
    scheduleOf (pref ++ extension) instrs 
    ¬ scheduleValid TC (pref ++ extension) σ₀

Frederick Pu (Oct 05 2025 at 22:11):

a lot of proofs can be done via a starvation invariant which is basically like a loop invariant but you also have to prove it won't starve ie starvationInv threadIdx \sigma -> match step \sigma \beta | x => starvationInv threadIdx x | none => False


Last updated: Dec 20 2025 at 21:32 UTC