Skip to content
Kavachकवच

Shared State

Also known as: weak reference weak pointer cycle leak memory leak reference cycle back link parent pointer KAVACH_CHECK_LEAKS shared mutable state mutex lock arc rc

● Advanced4 minBefore this: Values, Not ReferencesSpec 6.2

When several parts of a program must change the same thing, say so with Shared<T>: the one value that is shared.

fn main() {
    const counter = Shared(0)
    counter.update(n => n + 1)
    counter.update(n => n + 10)
    print(counter.get())
}

The program ran and printed:

kavach run shared.kav

11
shared.kav: Kavach code, then what running it prints
  • Shared(0) makes a shared value. update changes it, get reads it.
  • update holds the value's lock while its function runs, so updates from many tasks, on any number of cores, never lose one.
  • Seeing Shared in code tells every reader: this one is shared.

What update gives

update gives the new value, read while the lock is still held, so no other task can change it in between. It may be left unused. update_with takes a function that gives a pair, the new value and a result, and gives the result: take something and know what you took, in one step:

fn main() {
    const next_id = Shared(1)
    const id = next_id.update_with(n => (n + 1, n))
    print("took id {id}, the next is {next_id.get()}")

    const counter = Shared(0)
    counter.update(n => n + 1)
    const total = counter.update(n => n + 10)
    print("the count is now {total}")
}

The program ran and printed:

kavach run update-gives.kav

took id 1, the next is 2the count is now 11
update-gives.kav: Kavach code, then what running it prints

The function given to update can't wait

While update's function runs, every other task that wants the value waits for it. A function that sleeps, reads a file, calls the network or waits for a task would hold them all up, or lock two tasks against each other for good. So a wait there is refused:

use std.time

fn main() {
    const counter = Shared(0)
    counter.update(n => {
        time.sleep_ms(10)
        n + 1
    })
}

The compiler refused the program:

kavach run update-wait.kav

error: `time.sleep_ms` waits, and the function given to `update` can't wait: the `Shared` value stays locked until it returns. Get what the update needs first, then update (spec 6.2)
update-wait.kav: Kavach code, then what running it prints

The fix: get what the update needs first, then update. The lock is held only for the quick change:

use std.time

fn slow_count() -> Int uses Clock {
    time.sleep_ms(10)
    1
}

fn main() {
    const counter = Shared(0)
    const _ = [1, 2, 3, 4].parallel_map(n => {
        const extra = slow_count()
        counter.update(c => c + extra)
        n
    })
    print(counter.get())
}

The program ran and printed:

kavach run update-first.kav

4
update-first.kav: Kavach code, then what running it prints

A wait hidden inside another function can't be seen while compiling, so it stops the program instead, with the same advice:

use std.time

fn slow_count() -> Int uses Clock {
    time.sleep_ms(10)
    1
}

fn main() {
    const counter = Shared(0)
    counter.update(c => c + slow_count())
    print(counter.get())
}

The program ran, then stopped with a panic:

kavach run update-through.kav

panic: can't wait inside `update`: the `Shared` value stays locked until its function returns. Get what the update needs first, then update (spec 6.2)
update-through.kav: Kavach code, then what running it prints

A panic inside update

A panic in another task stops only that task. If it happens inside update's function, the value stays as it was, and other tasks can go on using it:

fn bonus(scores: [Int]) -> Int {
    scores[3]
}

fn add_bonus(total: Shared<Int>) {
    total.update(n => n + bonus([5]))
}

fn main() {
    const total = Shared(10)
    const t = spawn cores add_bonus(total)
    t.wait() catch _ => {}
    print("total {total.get()}")
    total.update(n => n + 1)
    print("total {total.get()}")
}

The program ran and printed:

kavach run update-panic.kav

total 10total 11
update-panic.kav: Kavach code, then what running it prints
  • The panic is printed to standard error, with its place, so it isn't in the box above.
  • The half-done change is never seen: total is still 10, and the next update works.
a panic inside update, tested

A Shared that holds a way back to itself, directly or through others, makes a cycle, and nothing in a cycle is freed. Weak<T> points to a Shared without keeping it alive: s.weak() makes one, and w.shared() gives the Shared back while it lives, and none once it was freed:

type Room {
    name: String
    doors: Shared<[Door]>
}

type Door {
    to: Weak<Room>            // a door doesn't keep its room alive
}

fn main() {
    const no_doors: [Door] = []
    const hall = Shared(Room(name: "hall", doors: Shared(no_doors)))
    const back = hall.weak()
    hall.get().doors.update(doors => {
        let more = doors
        more.push(Door(to: back))
        more
    })
    if let room = back.shared() {
        print(room.get().name)
    }
    const lost = Shared(5).weak()   // nothing holds the 5
    print(lost)
    print(lost.shared() == none)
}

The program ran and printed:

kavach run weak.kav

hallWeak(none)true
weak.kav: Kavach code, then what running it prints
  • A door points back to its room with a Weak, so the room and its doors make no cycle.
  • A Shared lives until the end of the block that holds it. hall isn't read after the update, but lives to the end of main, so back.shared() still finds it. What w.shared() gives depends on the blocks, as written, not on where a Shared was last used.
  • A Weak prints as Weak(…), or Weak(none) once its value was freed.

A cycle is reported

There is no cycle collector: a leaked cycle is a bug to fix. kavach run and kavach test report each Shared still alive when the program ends, which only a cycle can keep, with the line that made it:

type Room {
    name: String
    doors: Shared<[Room]>
}

fn main() {
    const none_yet: [Room] = []
    const hall = Room(name: "hall", doors: Shared(none_yet))
    hall.doors.update(doors => {
        let more = doors
        more.push(hall)       // the hall holds itself: a cycle
        more
    })
    print("done")
}
src/main.kav: Kavach code
terminal
kavach run
done
leak: a `Shared` value made at src/main.kav:8 was never freed: a cycle keeps it
  make the link back a `Weak`, so no cycle forms (spec 6.2)

The report goes to standard error. A built program does the same when run with KAVACH_CHECK_LEAKS=1, and otherwise pays nothing for it.