12 KiB
12 KiB
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
- Bux API Layer (
lib/Task.bux) — thin wrappers around C extern functions - C Scheduler Runtime (
rt/green_threads.c) — the scheduler, context switcher, and signal handler - 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 queueSIGVTALRMfires → current task pauses → scheduler picks next task →swapcontextChannel_Recvon empty channel → task marked BLOCKED → yields → scheduler runs another task
3. Data Structures
Task
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
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
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 byTask::Wait
4. Bux API
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
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:
funcparameter is*voidbecause the C runtime does not know Bux types- Users cast their function to
*void(like a C function pointer) Task_Initis optional — the firstTask_Spawnauto-initializes with CPU core count workers
5. Scheduler Algorithm
Preemption
SIGVTALRMtimer set to 10ms interval viasetitimer(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):
- Checks its own
run_queue(LIFO — push/pop from head for cache locality) - If empty: attempts to "steal" from a random other worker (FIFO from tail)
- If all queues empty: sleeps on a condition variable
- 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()setsshutdown = 1- Workers exit their loop when no more tasks exist
- Main thread waits for all workers with
pthread_join
6. Context Switching
#include <ucontext.h>
#include <signal.h>
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
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_wrappercallsfunc(arg), then marks task as FINISHED and returns to scheduler
/* 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
- Unit tests (C level): Test queue operations, task creation, context switch in isolation
- 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
- Stress test: Spawn 1000+ tasks, verify no crashes or memory leaks
- 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/writeto 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) gokeyword as syntactic sugar forTask::Spawn