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
11Shared(0)makes a shared value.updatechanges it,getreads it.updateholds the value's lock while its function runs, so updates from many tasks, on any number of cores, never lose one.- Seeing
Sharedin 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
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)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
4A 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)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
- 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:
totalis still10, and the nextupdateworks.
Links back: Weak
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
- A door points back to its room with a
Weak, so the room and its doors make no cycle. - A
Sharedlives until the end of the block that holds it.hallisn't read after the update, but lives to the end ofmain, soback.shared()still finds it. Whatw.shared()gives depends on the blocks, as written, not on where aSharedwas last used. - A
Weakprints asWeak(…), orWeak(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")
}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.