threads(M2): thread_spawn/thread_exit + runtime.Thread.spawn
A thread is a task sharing the caller's address space. New private syscalls thread_spawn(entry, stack_top, arg)=37 and thread_exit=38: thread_spawn goes through scheduler.spawnThread (retains the shared aspace), thread_exit ends the task like a process exit(0) (terminateCurrent -> releaseAspace, so the space survives while siblings hold it). The closure pointer reaches the new thread in rdi via a new jump_to_user_arg asm path and a per-task user_arg (0 for a normal process, whose _start ignores it) - so the runtime trampoline is a plain C-ABI Zig function, no naked asm. runtime.Thread (library/runtime/thread.zig) mirrors std.Thread.spawn: mmap a stack, heap-allocate the args closure, hand the kernel the trampoline + closure. addThreadedUserBinary opts a binary into single_threaded=false; thread-test is the first, and proves a worker runs in the shared address space via a shared global the main thread polls. Gate thread-spawn PASS; 16 guardrail cases green (incl. args/init/process on the new jump_to_user_arg path) + aspace-refcount; build + host tests clean.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
//! thread-test — the first multi-threaded danos binary (docs/threading-plan.md M2).
|
||||
//!
|
||||
//! Proves `runtime.Thread.spawn` starts a task in the **same address space**: the main
|
||||
//! thread spawns a worker, the worker writes a shared global and signals `done`, and the
|
||||
//! main thread — polling that shared memory — observes the write. Seeing the write proves
|
||||
//! the two tasks share one address space (a separate process could not touch this memory).
|
||||
//! The `thread-test: child ran in shared aspace ok` line is the case's marker.
|
||||
//!
|
||||
//! Built multi-threaded (`addThreadedUserBinary`), so the poll below is a real atomic
|
||||
//! load the compiler must re-read — under a single-threaded build it could be hoisted.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
|
||||
/// Written by the worker thread, read by main — the shared-address-space evidence.
|
||||
var shared_value: u32 = 0;
|
||||
/// Release/acquire handshake: publishes the `shared_value` write to the reader.
|
||||
var done = std.atomic.Value(u32).init(0);
|
||||
|
||||
const sentinel: u32 = 0xA5A5;
|
||||
|
||||
fn worker() void {
|
||||
shared_value = sentinel; // a plain write to a global we share with main
|
||||
done.store(1, .release); // ...published by this release store
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
_ = runtime.system.write("thread-test: starting\n");
|
||||
|
||||
_ = runtime.Thread.spawn(.{}, worker, .{}) catch {
|
||||
_ = runtime.system.write("thread-test: FAIL spawn refused\n");
|
||||
return;
|
||||
};
|
||||
|
||||
// Bounded wait for the worker to run and publish. yield() keeps the core useful;
|
||||
// the acquire load pairs with the worker's release store.
|
||||
var spins: usize = 0;
|
||||
while (done.load(.acquire) == 0 and spins < 50_000_000) : (spins += 1) {
|
||||
runtime.system.yield();
|
||||
}
|
||||
|
||||
if (done.load(.acquire) == 1 and shared_value == sentinel) {
|
||||
_ = runtime.system.write("thread-test: child ran in shared aspace ok\n");
|
||||
} else {
|
||||
_ = runtime.system.write("thread-test: FAIL worker did not update shared memory\n");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user