thread
Methods on every thread.
What a thread is
A thread is a cooperative coroutine, not an operating system thread. Nothing runs in parallel and nothing needs a lock: a thread runs only after a caller has resumed it, and control returns to that caller when the thread suspends.
What separates a thread from a generator is the stack. A generator yields from its own body and nowhere else, so a helper it calls cannot pause it. A thread has an execution stack of its own, so suspend works from any depth: a function three calls down can suspend the thread, and resuming it continues from that point. A thread also carries its own error handler, so a failure inside it does not have to be handled by the same policy as the code that started it.
The handshake
Values pass in both directions, and each side reads them at a different place:
- call starts the thread; its arguments become the parameters of the thread function.
suspend(x)pauses the thread.xbecomes the return value of thecallor wakeup that was waiting.wakeup(y)resumes it.ybecomes the return value of thesuspendthat paused it.- When the thread function returns, that value is the return value of the last
wakeup.
So a resumed thread reads its input from suspend's result, not from a parameter, and the caller reads the thread's output from wakeup's result. When this rule is forgotten, the values seem to arrive one step late.
// A loader that reports progress. The nested call is the point: a generator
// could not suspend from in there, a thread can.
function readChunk(name) {
let ack = suspend($"loading {name}")
return $"{name} ({ack})"
}
function loadAll(first, second) {
let a = readChunk(first)
let b = readChunk(second)
return $"done: {a}, {b}"
}
let loader = newthread(loadAll)
println(loader.getstatus())
// call's arguments are the thread function's parameters, and it returns
// whatever the first suspend handed over
println(loader.call("terrain", "props"))
println(loader.getstatus())
// wakeup's argument becomes the return value of that suspend
println(loader.wakeup("ok"))
println(loader.wakeup("ok"))
println(loader.getstatus())idle
loading terrain
suspended
loading props
done: terrain (ok), props (ok)
idleStatus
getstatus reports "idle" before the first call and again after the thread function returns, "suspended" while it waits, and "running" when asked from inside the thread itself. A thread that ended cannot be restarted; make a new one with newthread.
Failure
An unhandled throw inside a thread reaches the caller that resumed it, so call and wakeup can both throw and can be wrapped in try. It passes through the VM error handler on the way, which prints the message and the thread's own call stack, so a failing thread still prints to the log even when the caller catches it. A thread that failed is left "idle", the same as if it had returned.
wakeupthrow is the counterpart of wakeup: instead of returning a value to suspend, it raises one there, so the thread's own try blocks see it. This is how a caller cancels a suspended thread from outside.
suspend called on the root VM rather than inside a thread throws cannot suspend the root vm.
Functions
| call | thread.call(...): any | Starts the thread's function, passing every argument straight through to it. |
| clone | clone(): any | Returns the thread itself: a thread has no separate clone identity. |
| getstackinfos | thread.getstackinfos(level: number): any | Returns call stack information from inside a suspended thread, for the given stack level of that thread (not of the caller). |
| getstatus | thread.getstatus(): any | Returns the thread's current state as a string. |
| tostring | tostring(): any | Returns a string that names the type and identity of the thread. |
| wakeup | thread.wakeup(...): any | Resumes a suspended thread, passing one value back to its suspend call. |
| wakeupthrow | thread.wakeupthrow(value, [rethrow: bool], ...): any | Resumes a suspended thread by throwing value at its suspend call, instead of returning from it normally. |
| weakref | weakref(): any | Returns a weak reference to the thread. |