From 6a0a0425dd85ab464877f72da4bea49cfc872576 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Wed, 10 Jun 2026 19:33:53 +0300 Subject: [PATCH] docs: add green threads / task scheduler design document --- .../specs/2026-06-10-green-threads-design.md | 390 ++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-10-green-threads-design.md diff --git a/docs/superpowers/specs/2026-06-10-green-threads-design.md b/docs/superpowers/specs/2026-06-10-green-threads-design.md new file mode 100644 index 0000000..804ab5d --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-green-threads-design.md @@ -0,0 +1,390 @@ +# Bux Green Threads / Task Scheduler — Design Document + +> **Date:** 2026-06-10 +> **Status:** Design Approved +> **Scope:** MVP preemptive M:N green thread scheduler with work-stealing + +--- + +## 1. Overview + +Bux will gain Go-style green threads (M:N scheduling) without garbage collection. A fixed pool of OS worker threads runs a larger number of lightweight "green" tasks, preemptively scheduled via `SIGVTALRM` and context switching via `ucontext`. + +### Goals +- Enable concurrent programming in Bux with Go-like ergonomics +- Zero GC pauses (Bux is manually managed) +- Work-stealing for balanced CPU utilization across cores +- Minimal language changes (pure stdlib + C runtime addition) + +### Non-Goals (for MVP) +- Cross-platform support beyond Linux/macOS (ucontext is POSIX) +- Dynamic stack growth (fixed-size stacks) +- I/O polling integration (epoll/kqueue) — channels + sleep only +- Task cancellation / timeouts + +--- + +## 2. Architecture + +``` +┌─────────────────────────────────────────┐ +│ Bux Source Code (.bux) │ +│ func Main() { Task::Spawn(Worker); } │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Bux Stdlib — `lib/Task.bux` │ +│ extern func bux_task_spawn(...); │ +│ func Task::Spawn(f) { ... } │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Generated C Code (from buxc) │ +│ bux_task_spawn(worker_func, arg); │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ C Runtime — `rt/green_threads.c` │ +│ • Scheduler (M:N, work-stealing) │ +│ • ucontext context switch │ +│ • SIGVTALRM preemption │ +│ • Per-OS-thread run queues │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ OS Threads (pthread) │ +│ Worker 0 │ Worker 1 │ Worker 2 │ ... │ +│ ┌─────┐ │ ┌─────┐ │ ┌─────┐ │ +│ │TaskA│ │ │TaskB│ │ │TaskC│ │ +│ │TaskD│ │ │ │ │ │TaskE│ │ +│ └─────┘ │ └─────┘ │ └─────┘ │ +└─────────────────────────────────────────┘ +``` + +### Components + +1. **Bux API Layer** (`lib/Task.bux`) — thin wrappers around C extern functions +2. **C Scheduler Runtime** (`rt/green_threads.c`) — the scheduler, context switcher, and signal handler +3. **OS Worker Threads** — pthreads, each executing green threads from its local queue + +### Data Flow +- `Task::Spawn(func, arg)` → `bux_task_spawn(func, arg)` → creates Task + ucontext → added to run queue +- `SIGVTALRM` fires → current task pauses → scheduler picks next task → `swapcontext` +- `Channel_Recv` on empty channel → task marked BLOCKED → yields → scheduler runs another task + +--- + +## 3. Data Structures + +### Task + +```c +typedef enum { + TASK_READY, + TASK_RUNNING, + TASK_BLOCKED, + TASK_FINISHED, +} TaskState; + +typedef struct Task { + ucontext_t ctx; /* ucontext for context switch */ + void *stack; /* malloc'd stack */ + size_t stack_size; /* e.g., 256KB */ + + void (*func)(void*); /* Entry function */ + void *arg; /* Argument */ + + TaskState state; + int id; /* Unique task ID */ + struct Task *next; /* Linked list for queues */ + + /* Blocking state */ + void *waiting_on; /* Channel handle, if blocked on recv */ + int64_t wake_at; /* Timestamp (ms) for sleep wake-up */ +} Task; +``` + +### Per-Worker Scheduler + +```c +typedef struct Scheduler { + Task *run_queue_head; /* Ready tasks (LIFO: push/pop head) */ + Task *run_queue_tail; + int queue_count; + + Task *current; /* Currently running task */ + pthread_t os_thread; /* OS thread handle */ + int worker_id; /* 0 .. N-1 */ + + struct Scheduler **all_schedulers; /* For work-stealing */ + int num_workers; +} Scheduler; +``` + +### Global State + +```c +typedef struct TaskPool { + Scheduler **schedulers; /* One per worker thread */ + int num_workers; /* Default = CPU core count */ + pthread_mutex_t spawn_lock; + int next_task_id; + int shutdown; /* Set to 1 for graceful shutdown */ +} TaskPool; +``` + +**Key Decisions:** +- Linked list queues — simple, coarse-grained lock for MVP (lock-free atomic ops later) +- Fixed 256KB stacks — sufficient for most code, guard page for overflow detection +- Task ID returned by `Task::Spawn`, consumed by `Task::Wait` + +--- + +## 4. Bux API + +```bux +module Std::Task { + +extern func bux_task_init(num_workers: int) -> int; +extern func bux_task_spawn(func: *void, arg: *void) -> int; +extern func bux_task_wait(task_id: int); +extern func bux_task_sleep(ms: int64); +extern func bux_task_yield(); +extern func bux_task_current_id() -> int; +extern func bux_task_shutdown(); + +struct TaskHandle { + id: int; +} + +func Task_Spawn(func: *void, arg: *void) -> TaskHandle { + let id: int = bux_task_spawn(func, arg); + return TaskHandle { id: id }; +} + +func Task_Wait(handle: TaskHandle) { + bux_task_wait(handle.id); +} + +func Task_Sleep(ms: int64) { + bux_task_sleep(ms); +} + +func Task_Yield() { + bux_task_yield(); +} + +func Task_CurrentId() -> int { + return bux_task_current_id(); +} + +func Task_Init(num_workers: int) -> int { + return bux_task_init(num_workers); +} + +func Task_Shutdown() { + bux_task_shutdown(); +} + +} +``` + +### Usage Example + +```bux +import Std::Task; +import Std::Channel; + +func Worker(id: int) { + PrintLine("Worker " + Int_ToString(id) + " starting"); + Task_Sleep(100); + PrintLine("Worker " + Int_ToString(id) + " done"); +} + +func Main() -> int { + Task_Init(4); + + let h1: TaskHandle = Task_Spawn(Worker as *void, 1 as *void); + let h2: TaskHandle = Task_Spawn(Worker as *void, 2 as *void); + + Task_Wait(h1); + Task_Wait(h2); + + Task_Shutdown(); + return 0; +} +``` + +**Notes:** +- `func` parameter is `*void` because the C runtime does not know Bux types +- Users cast their function to `*void` (like a C function pointer) +- `Task_Init` is optional — the first `Task_Spawn` auto-initializes with CPU core count workers + +--- + +## 5. Scheduler Algorithm + +### Preemption + +- `SIGVTALRM` timer set to 10ms interval via `setitimer(ITIMER_VIRTUAL, ...)` +- Signal handler calls `schedule()` — saves current context, selects next task +- Fixed 10ms quantum for MVP (configurable later) + +### Work-Stealing + +Each OS thread (worker): +1. Checks its own `run_queue` (LIFO — push/pop from head for cache locality) +2. If empty: attempts to "steal" from a random other worker (FIFO from tail) +3. If all queues empty: sleeps on a condition variable +4. When a new task is spawned: signals the condition variable to wake a sleeper + +### Task Selection (per worker) + +``` +schedule(): + 1. If current task is RUNNING → mark as READY, push to queue + 2. Check sleep queue — any wake_time expired? + 3. Check blocked tasks — any channel now has data? + 4. Pop READY task from queue (round-robin within queue) + 5. Mark as RUNNING + 6. swapcontext() to new task +``` + +### Graceful Shutdown + +- `Task_Shutdown()` sets `shutdown = 1` +- Workers exit their loop when no more tasks exist +- Main thread waits for all workers with `pthread_join` + +--- + +## 6. Context Switching + +```c +#include +#include + +static void timer_handler(int sig) { + (void)sig; + schedule(); +} + +void scheduler_init(void) { + struct sigaction sa; + sa.sa_handler = timer_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGVTALRM, &sa, NULL); + + struct itimerval itv; + itv.it_interval.tv_sec = 0; + itv.it_interval.tv_usec = 10000; /* 10ms */ + itv.it_value = itv.it_interval; + setitimer(ITIMER_VIRTUAL, &itv, NULL); +} + +void task_switch(Task *from, Task *to) { + from->state = TASK_READY; + to->state = TASK_RUNNING; + current_task = to; + swapcontext(&from->ctx, &to->ctx); +} +``` + +### Task Creation + +```c +Task* task_create(void (*func)(void*), void *arg) { + Task *t = calloc(1, sizeof(Task)); + t->stack = malloc(STACK_SIZE); + t->stack_size = STACK_SIZE; + t->func = func; + t->arg = arg; + t->state = TASK_READY; + + getcontext(&t->ctx); + t->ctx.uc_stack.ss_sp = t->stack; + t->ctx.uc_stack.ss_size = t->stack_size; + t->ctx.uc_link = &scheduler_context; + + /* makecontext only accepts int arguments; use thread-local to pass pointers */ + bux_task_creating = t; + makecontext(&t->ctx, task_entry_wrapper, 0); + bux_task_creating = NULL; + + return t; +} +``` + +--- + +## 7. Stack Management + +- **Size:** 256KB default, configurable via `Task_Init` +- **Allocation:** `malloc()` + guard page (`mprotect(..., PROT_NONE)`) for overflow detection +- **Entry wrapper:** `task_entry_wrapper` calls `func(arg)`, then marks task as FINISHED and returns to scheduler + +```c +/* Thread-local pointer to the task being created (for makecontext wrapper) */ +static __thread Task *bux_task_creating; + +static void task_entry_wrapper(void) { + Task *t = bux_task_creating; + t->func(t->arg); + t->state = TASK_FINISHED; + schedule(); /* Never returns */ +} +``` + +**Cleanup:** `Task_Wait()` frees the stack and Task struct when the task completes. + +--- + +## 8. Integration with Bux Build System + +The C runtime file `rt/green_threads.c` is compiled and linked alongside `rt/runtime.c` and `rt/io.c`: + +``` +bux build: + 1. Merge all .bux → single .bux + 2. Compile .bux → .c (buxc) + 3. Compile generated .c + rt/*.c → binary (gcc/clang) +``` + +No compiler changes are required. The scheduler is purely a runtime addition. + +--- + +## 9. Error Handling & Edge Cases + +| Scenario | Handling | +|----------|----------| +| `Task_Spawn` when scheduler not initialized | Auto-initialize with CPU core count | +| Stack overflow | Guard page triggers SIGSEGV (MVP: abort; future: recoverable) | +| `Task_Wait` on non-existent ID | No-op / warning (task already finished) | +| All workers blocked | Main thread busy-waits or sleeps (MVP: simple spin) | +| `Task_Shutdown` with running tasks | Wait for all tasks to finish, then join workers | + +--- + +## 10. Testing Strategy + +1. **Unit tests (C level):** Test queue operations, task creation, context switch in isolation +2. **Integration tests (Bux):** + - Spawn 2 tasks, wait for both + - Spawn N tasks, verify they all run + - Channel send/recv between concurrent tasks + - Sleep test — verify other tasks run while one sleeps +3. **Stress test:** Spawn 1000+ tasks, verify no crashes or memory leaks +4. **Selfhost loop:** Verify the scheduler does not break compiler determinism + +--- + +## 11. Future Work (Post-MVP) + +- **Cross-platform context switching:** Replace ucontext with `setjmp` + manual stack switch for Windows +- **Dynamic stack growth:** Detect near-overflow and realloc stack +- **I/O integration:** Hook `read`/`write` to yield on blocking I/O +- **Work-stealing lock-free queues:** Replace coarse locks with atomic operations +- **Task cancellation / timeouts:** `Task_Cancel(handle)`, `Task_Wait(handle, timeout_ms)` +- `go` keyword as syntactic sugar for `Task::Spawn`