Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
# goroutines
> `goroutines` is experimental.
> It landed after `0.5.1`, so `import goroutines` is not available in that release.
Go-style goroutine runtime for V, implementing the GMP
(Goroutine-Machine-Processor) scheduling model translated
from the Go runtime (`src/runtime/proc.go`, `runtime2.go`,
`chan.go`).
## Overview
This module provides lightweight goroutines for V's `go`
keyword, as opposed to `spawn` which creates OS threads.
### GMP Model
- **G (Goroutine)**: Lightweight unit of execution with its own stack (~8KB default)
- **M (Machine)**: OS thread that executes goroutines
- **P (Processor)**: Logical processor with a local run queue (one per CPU core)
### Key Features
- **Work stealing**: Idle processors steal work from busy ones
- **Local run queues**: Lock-free per-P queues minimize contention
- **Global run queue**: Overflow and fairness mechanism
- **Goroutine parking**: Efficient blocking/unblocking for channels
- **G reuse**: Dead goroutines are recycled to reduce allocation
## Usage
```v ignore
// `go` launches a goroutine (lightweight, scheduled by GMP)
go expensive_computation()
// `spawn` launches an OS thread (traditional V behavior)
spawn blocking_io_task()
```
## Architecture
Translated from Go's runtime source:
| Go Source | V Module File | Purpose |
|-----------|---------------|---------|
| `runtime2.go` | `goroutines.v` | Core data structures (G, M, P, Sched) |
| `proc.go` | `scheduler.v` | Scheduler loop, work stealing, run queues |
| `proc.go` | `park.v` | gopark/goready, Sudog, WaitQ |
| `proc.go` | `init.v` | Initialization (schedinit, procresize) |
| `chan.go` | `chan.v` | Channel implementation |
| asm (gogo/gosave) | `context_nix.c.v` | Context switching (ucontext) |
| asm (gogo/gosave) | `context_windows.c.v` | Context switching (Windows fibers) |
## References
- [Go Scheduler Design Doc](https://golang.org/s/go11sched)
- [Go Runtime Source](https://github.com/golang/go/tree/master/src/runtime)
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Atomic operations and C interop for the goroutine scheduler.
module goroutines
#include <stdlib.h>
#include <string.h>
#include "@VMODROOT/vlib/goroutines/goroutines_tls.h"
#flag @VMODROOT/vlib/goroutines/tls.c
// Thread-local storage
fn C.goroutines_get_current_m() voidptr
fn C.goroutines_set_current_m(mp voidptr)
// Typed atomic operations (implemented in tls.c)
fn C.goroutines_atomic_load_u32(ptr &u32) u32
fn C.goroutines_atomic_store_u32(ptr &u32, val u32)
fn C.goroutines_atomic_fetch_add_u32(ptr &u32, val u32) u32
fn C.goroutines_atomic_fetch_add_i32(ptr &i32, val i32) i32
fn C.goroutines_atomic_fetch_sub_i32(ptr &i32, val i32) i32
fn C.goroutines_atomic_fetch_add_u64(ptr &u64, val u64) u64
fn C.goroutines_atomic_cas_u32(ptr &u32, expected &u32, desired u32) bool
fn C.goroutines_atomic_cas_ptr(ptr voidptr, expected voidptr, desired voidptr) bool
fn C.grt_spinlock_lock(lk &i32)
fn C.grt_spinlock_unlock(lk &i32)
fn C.memcpy(dest voidptr, src voidptr, n usize) voidptr
fn C.memset(dest voidptr, ch int, n usize) voidptr
fn C.rand() int
// SpinLock - ucontext-safe lock (pthreads mutex breaks with swapcontext).
pub struct SpinLock {
mut:
state i32
}
pub fn (mut s SpinLock) acquire() {
C.grt_spinlock_lock(&s.state)
}
pub fn (mut s SpinLock) release() {
C.grt_spinlock_unlock(&s.state)
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Channel implementation for goroutines.
// Translated from Go's runtime/chan.go.
//
// Channels provide goroutine-safe communication between goroutines.
// They support both buffered and unbuffered modes.
//
// Key operations translated from Go:
// - makechan() -> chan_make()
// - chansend() -> chan_send()
// - chanrecv() -> chan_recv()
// - closechan() -> chan_close()
module goroutines
// Chan is a goroutine-safe channel for communication between goroutines.
// Translated from Go's hchan struct in chan.go.
pub struct Chan {
pub mut:
mu SpinLock // protects all fields (spinlock is ucontext-safe)
qcount u32 // total data in the queue
dataqsiz u32 // size of the circular buffer
buf voidptr // circular buffer for buffered channels
elemsize u16 // size of each element
closed bool // true if channel is closed
sendx u32 // send index into circular buffer
recvx u32 // receive index into circular buffer
recvq WaitQ // list of recv waiters
sendq WaitQ // list of send waiters
}
// chan_make creates a new channel.
// If buf_size > 0, creates a buffered channel.
// Translated from Go's makechan() in chan.go.
pub fn chan_make(elem_size int, buf_size int) &Chan {
mut c := &Chan{
elemsize: u16(elem_size)
dataqsiz: u32(buf_size)
}
if buf_size > 0 {
c.buf = unsafe { malloc(elem_size * buf_size) }
}
return c
}
// chan_send sends a value on the channel.
// If block is true, blocks until the send can proceed.
// Returns true if the value was sent.
// Translated from Go's chansend() in chan.go.
pub fn chan_send(c &Chan, ep voidptr, block bool) bool {
if c == unsafe { nil } {
if !block {
return false
}
// Block forever on nil channel (Go behavior)
gopark('chan send (nil chan)')
return false // unreachable
}
mut ch := unsafe { c }
ch.mu.acquire()
if ch.closed {
ch.mu.release()
panic('send on closed channel')
}
// Fast path: try to find a waiting receiver
sg := ch.recvq.dequeue()
if sg != unsafe { nil } {
// Found a waiting receiver - send directly
ch.mu.release()
send_direct(sg, ep, ch.elemsize)
return true
}
// Buffered channel with space available
if ch.qcount < ch.dataqsiz {
// Put data in buffer
dst := chan_buf(ch, ch.sendx)
unsafe { C.memcpy(dst, ep, ch.elemsize) }
ch.sendx++
if ch.sendx == ch.dataqsiz {
ch.sendx = 0
}
ch.qcount++
ch.mu.release()
return true
}
if !block {
ch.mu.release()
return false
}
// Block: enqueue ourselves on the send wait queue
gp := get_current_g()
mut mysg := &Sudog{
g: unsafe { gp }
elem: ep
c: voidptr(ch)
}
ch.sendq.enqueue(mysg)
ch.mu.release()
// Park the goroutine until a receiver wakes us
gopark('chan send')
return true
}
// chan_recv receives a value from the channel.
// If block is true, blocks until a value is available.
// Returns (received, ok). ok is false if channel is closed and empty.
// Translated from Go's chanrecv() in chan.go.
pub fn chan_recv(c &Chan, ep voidptr, block bool) (bool, bool) {
if c == unsafe { nil } {
if !block {
return false, false
}
gopark('chan receive (nil chan)')
return false, false // unreachable
}
mut ch := unsafe { c }
ch.mu.acquire()
// Fast path: try to find a waiting sender
sg := ch.sendq.dequeue()
if sg != unsafe { nil } {
ch.mu.release()
recv_direct(ch, sg, ep)
return true, true
}
// Buffered channel with data available
if ch.qcount > 0 {
src := chan_buf(ch, ch.recvx)
if ep != unsafe { nil } {
unsafe { C.memcpy(ep, src, ch.elemsize) }
}
ch.recvx++
if ch.recvx == ch.dataqsiz {
ch.recvx = 0
}
ch.qcount--
ch.mu.release()
return true, true
}
if ch.closed {
ch.mu.release()
if ep != unsafe { nil } {
unsafe { C.memset(ep, 0, ch.elemsize) }
}
return true, false
}
if !block {
ch.mu.release()
return false, false
}
// Block: enqueue ourselves on the recv wait queue
gp := get_current_g()
mut mysg := &Sudog{
g: unsafe { gp }
elem: ep
c: voidptr(ch)
}
ch.recvq.enqueue(mysg)
ch.mu.release()
// Park until a sender wakes us
gopark('chan receive')
return true, true
}
// chan_close closes the channel.
// Translated from Go's closechan() in chan.go.
pub fn chan_close(c &Chan) {
if c == unsafe { nil } {
panic('close of nil channel')
}
mut ch := unsafe { c }
ch.mu.acquire()
if ch.closed {
ch.mu.release()
panic('close of closed channel')
}
ch.closed = true
// Wake all waiting receivers
for {
mut sg := ch.recvq.dequeue()
if sg == unsafe { nil } {
break
}
if sg.elem != unsafe { nil } {
unsafe { C.memset(sg.elem, 0, ch.elemsize) }
}
sg.success = false
goready(sg.g)
}
// Wake all waiting senders (they will panic)
for {
mut sg := ch.sendq.dequeue()
if sg == unsafe { nil } {
break
}
sg.success = false
goready(sg.g)
}
ch.mu.release()
}
// send_direct sends data directly from sender to a waiting receiver.
// Translated from Go's send() in chan.go.
fn send_direct(sg &Sudog, ep voidptr, elem_size u16) {
if sg.elem != unsafe { nil } {
unsafe { C.memcpy(sg.elem, ep, elem_size) }
}
mut s := unsafe { sg }
s.success = true
goready(sg.g)
}
// recv_direct receives data directly from a waiting sender.
fn recv_direct(ch &Chan, sg &Sudog, ep voidptr) {
if ch.dataqsiz == 0 {
// Unbuffered: copy directly from sender
if ep != unsafe { nil } {
unsafe { C.memcpy(ep, sg.elem, ch.elemsize) }
}
} else {
// Buffered: take from buffer, then copy sender's data into buffer
buf_elem := chan_buf(ch, unsafe { ch }.recvx)
if ep != unsafe { nil } {
unsafe { C.memcpy(ep, buf_elem, ch.elemsize) }
}
unsafe { C.memcpy(buf_elem, sg.elem, ch.elemsize) }
unsafe {
ch.recvx++
if ch.recvx == ch.dataqsiz {
ch.recvx = 0
}
ch.sendx = ch.recvx
}
}
mut s := unsafe { sg }
s.success = true
goready(sg.g)
}
// chan_buf returns a pointer to the i-th slot in the buffer.
// Translated from Go's chanbuf() in chan.go.
fn chan_buf(c &Chan, i u32) voidptr {
return unsafe { voidptr(usize(c.buf) + usize(i) * usize(c.elemsize)) }
}
// chan_len returns the number of elements in the channel buffer.
pub fn chan_len(c &Chan) int {
if c == unsafe { nil } {
return 0
}
return int(c.qcount)
}
// chan_cap returns the capacity of the channel buffer.
pub fn chan_cap(c &Chan) int {
if c == unsafe { nil } {
return 0
}
return int(c.dataqsiz)
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Platform-specific context switching using ucontext (Linux, macOS, BSDs).
// This provides the low-level mechanism for goroutine context switches,
// analogous to Go's gogo/gosave assembly routines.
module goroutines
#flag -D_XOPEN_SOURCE=700
#flag darwin -D_DARWIN_C_SOURCE
#include "@VMODROOT/vlib/goroutines/context_nix.h"
fn C.goroutines_context_alloc() voidptr
fn C.goroutines_context_init(ctx voidptr, stack voidptr, stack_size int, entry_fn voidptr, arg voidptr)
fn C.goroutines_context_switch(from voidptr, to voidptr)
fn C.goroutines_context_set(to voidptr)
// Context wraps ucontext_t for goroutine context switching.
// The actual ucontext_t is heap-allocated in C to avoid issues with
// ucontext_t being a typedef rather than a struct tag.
pub struct Context {
pub mut:
uctx voidptr // heap-allocated ucontext_t
}
// context_init initializes a context for a new goroutine.
// Sets up the context to run `entry_fn` with `arg` on the given stack.
pub fn context_init(mut ctx Context, stack voidptr, stack_size int, entry_fn fn (voidptr), arg voidptr) {
if ctx.uctx == unsafe { nil } {
ctx.uctx = C.goroutines_context_alloc()
}
C.goroutines_context_init(ctx.uctx, stack, stack_size, voidptr(entry_fn), arg)
}
// context_switch saves the current context into `from` and switches to `to`.
pub fn context_switch(mut from Context, to &Context) {
if from.uctx == unsafe { nil } {
from.uctx = C.goroutines_context_alloc()
}
C.goroutines_context_switch(from.uctx, to.uctx)
}
// context_set switches to the given context without saving.
pub fn context_set(to &Context) {
C.goroutines_context_set(to.uctx)
}
+69
View File
@@ -0,0 +1,69 @@
#ifndef GOROUTINES_CONTEXT_NIX_H
#define GOROUTINES_CONTEXT_NIX_H
#include <ucontext.h>
#include <stdint.h>
#include <stdlib.h>
// Extended context: ucontext_t + entry function info for first-time starts.
typedef struct {
ucontext_t uctx;
void (*entry_fn)(void *);
void *entry_arg;
} goroutine_ctx_t;
// Thread-local pointer to the target goroutine context.
// Set just before swapcontext/setcontext so the trampoline can find its fn/arg.
#if defined(_WIN32) || defined(_WIN64)
static __declspec(thread) goroutine_ctx_t *_goroutine_start_ctx = NULL;
#else
static _Thread_local goroutine_ctx_t *_goroutine_start_ctx = NULL;
#endif
static void *goroutines_context_alloc(void) {
return calloc(1, sizeof(goroutine_ctx_t));
}
// Zero-argument trampoline. Reads fn/arg from the TLS-stored context.
// This avoids passing arguments through makecontext, which is broken on macOS ARM64.
static void goroutines__context_trampoline(void) {
goroutine_ctx_t *gctx = _goroutine_start_ctx;
_goroutine_start_ctx = NULL;
void (*fn)(void *) = gctx->entry_fn;
void *arg = gctx->entry_arg;
// Clear so future context switches to this goroutine don't re-trigger
gctx->entry_fn = NULL;
gctx->entry_arg = NULL;
fn(arg);
}
static void goroutines_context_init(void *ctx_buf, void *stack, int stack_size, void *entry_fn, void *arg) {
goroutine_ctx_t *gctx = (goroutine_ctx_t *)ctx_buf;
gctx->entry_fn = (void (*)(void *))entry_fn;
gctx->entry_arg = arg;
getcontext(&gctx->uctx);
gctx->uctx.uc_stack.ss_sp = stack;
gctx->uctx.uc_stack.ss_size = (size_t)stack_size;
gctx->uctx.uc_link = NULL;
makecontext(&gctx->uctx, (void (*)())goroutines__context_trampoline, 0);
}
static void goroutines_context_switch(void *from_buf, void *to_buf) {
goroutine_ctx_t *from = (goroutine_ctx_t *)from_buf;
goroutine_ctx_t *to = (goroutine_ctx_t *)to_buf;
// If switching to a goroutine that hasn't started yet, set TLS for trampoline
if (to->entry_fn != NULL) {
_goroutine_start_ctx = to;
}
swapcontext(&from->uctx, &to->uctx);
}
static void goroutines_context_set(void *to_buf) {
goroutine_ctx_t *to = (goroutine_ctx_t *)to_buf;
if (to->entry_fn != NULL) {
_goroutine_start_ctx = to;
}
setcontext(&to->uctx);
}
#endif
@@ -0,0 +1,41 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Platform-specific context switching using Windows Fibers.
module goroutines
#include <windows.h>
fn C.CreateFiber(dwStackSize usize, lpStartAddress voidptr, lpParameter voidptr) voidptr
fn C.ConvertThreadToFiber(lpParameter voidptr) voidptr
fn C.SwitchToFiber(lpFiber voidptr)
fn C.DeleteFiber(lpFiber voidptr)
fn C.ConvertFiberToThread()
pub struct Context {
pub mut:
uctx voidptr // fiber handle on Windows
is_thread_fiber bool // true if this was created via ConvertThreadToFiber
}
pub fn context_init(mut ctx Context, stack voidptr, stack_size int, entry_fn fn (voidptr), arg voidptr) {
// Windows fibers manage their own stack, so we ignore the stack param
ctx.uctx = C.CreateFiber(usize(stack_size), voidptr(entry_fn), arg)
}
pub fn context_switch(mut from Context, to &Context) {
C.SwitchToFiber(to.uctx)
}
pub fn context_set(to &Context) {
C.SwitchToFiber(to.uctx)
}
// convert_thread_to_fiber must be called once per OS thread before using fibers.
pub fn convert_thread_to_fiber() Context {
mut ctx := Context{}
ctx.uctx = C.ConvertThreadToFiber(unsafe { nil })
ctx.is_thread_fiber = true
return ctx
}
@@ -0,0 +1,26 @@
// Example: Basic goroutine usage with `go` keyword.
//
// This demonstrates how V's `go` keyword launches goroutines
// using the GMP scheduler (translated from Go's runtime).
//
// `go` launches a lightweight goroutine (like Go's goroutines)
// `spawn` launches an OS thread (like V's traditional threads)
module main
import time
fn say(msg string) {
for i in 0 .. 5 {
println('${msg}: ${i}')
time.sleep(100 * time.millisecond)
}
}
fn main() {
// Launch goroutines with `go` - lightweight, Go-style concurrency
go say('goroutine 1')
go say('goroutine 2')
// Main goroutine continues running
say('main')
}
@@ -0,0 +1,119 @@
// Goroutine benchmark in Go – equivalent to goroutine_benchmark.v.
//
// Tests:
// 1. Goroutine creation + completion (fan-out/fan-in via unbuffered channel)
// 2. Channel ping-pong between two goroutines
// 3. Many goroutines contending on a single channel
//
// Run: go run goroutine_benchmark.go
package main
import (
"fmt"
"time"
)
// ---------------------------------------------------------------------------
// Benchmark 1 – fan-out / fan-in (unbuffered channel)
// ---------------------------------------------------------------------------
func benchFanOutFanIn(n int) {
c := make(chan int) // unbuffered, matching V benchmark
start := time.Now()
for i := 0; i < n; i++ {
go func() {
c <- 1
}()
}
for i := 0; i < n; i++ {
<-c
}
elapsed := time.Since(start)
fmt.Printf("fan-out/fan-in %d goroutines: %d us (%d ns/goroutine)\n",
n, elapsed.Microseconds(), elapsed.Nanoseconds()/int64(n))
}
// ---------------------------------------------------------------------------
// Benchmark 2 – channel ping-pong
// ---------------------------------------------------------------------------
func benchPingPong(n int) {
c1 := make(chan int, 1)
c2 := make(chan int, 1)
go func() {
for i := 0; i < n; i++ {
val := <-c1
c2 <- val + 1
}
}()
start := time.Now()
val := 0
for i := 0; i < n; i++ {
c1 <- val
val = <-c2
}
elapsed := time.Since(start)
fmt.Printf("ping-pong %d round-trips: %d us (%d ns/round-trip)\n",
n, elapsed.Microseconds(), elapsed.Nanoseconds()/int64(n))
}
// ---------------------------------------------------------------------------
// Benchmark 3 – contended channel (many producers, one consumer)
// ---------------------------------------------------------------------------
func benchContendedChannel(numProducers int, msgsPerProducer int) {
total := numProducers * msgsPerProducer
c := make(chan int, 64)
start := time.Now()
for p := 0; p < numProducers; p++ {
go func() {
for i := 0; i < msgsPerProducer; i++ {
c <- i
}
}()
}
for i := 0; i < total; i++ {
<-c
}
elapsed := time.Since(start)
fmt.Printf("contended chan %d producers x %d msgs: %d us (%d ns/msg)\n",
numProducers, msgsPerProducer,
elapsed.Microseconds(), elapsed.Nanoseconds()/int64(total))
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
func main() {
fmt.Println("=== Go Goroutine Benchmark ===")
fmt.Println()
benchFanOutFanIn(10)
benchFanOutFanIn(50)
benchFanOutFanIn(100)
benchFanOutFanIn(500)
fmt.Println()
benchPingPong(1000)
benchPingPong(10000)
benchPingPong(100000)
fmt.Println()
benchContendedChannel(4, 1000)
benchContendedChannel(10, 1000)
fmt.Println()
}
@@ -0,0 +1,157 @@
// Goroutine benchmark: measures scheduling and channel throughput
// of V's goroutine runtime (translated from Go's GMP scheduler).
//
// Compile: v -enable-globals -cc gcc -gc none -prod run goroutine_benchmark.v
// For comparison, see goroutine_benchmark.go.
//
// Note: Under the v1 compiler, the goroutines module uses pthreads-based
// sync.Mutex which has limitations with ucontext-based context switching.
// The v2 compiler will use a native spinlock, enabling higher scale.
module main
import goroutines
import sync
import time
__global bench_sem = sync.Semaphore{}
__global bench_n = int(0)
__global bench_chan1 = &goroutines.Chan(unsafe { nil })
__global bench_chan2 = &goroutines.Chan(unsafe { nil })
__global bench_msgs_per = int(0)
// --- fan-out/fan-in ---
fn fan_worker(arg voidptr) {
c := unsafe { &goroutines.Chan(arg) }
mut val := 1
goroutines.chan_send(c, voidptr(&val), true)
}
fn fan_collector(arg voidptr) {
c := unsafe { &goroutines.Chan(arg) }
mut recv_val := 0
for _ in 0 .. bench_n {
goroutines.chan_recv(c, voidptr(&recv_val), true)
}
bench_sem.post()
}
fn bench_fan_out_fan_in(n int) {
bench_n = n
c := goroutines.chan_make(int(sizeof(int)), 0)
sw := time.new_stopwatch()
goroutines.goroutine_create(voidptr(&fan_collector), voidptr(c), 0)
for _ in 0 .. n {
goroutines.goroutine_create(voidptr(&fan_worker), voidptr(c), 0)
}
bench_sem.wait()
elapsed := sw.elapsed()
us := elapsed.microseconds()
ns_per := elapsed.nanoseconds() / n
C.printf(c'fan-out/fan-in %d goroutines: %lld us (%lld ns/goroutine)\n', n, us, ns_per)
}
// --- ping-pong ---
fn ping_side(arg voidptr) {
mut val := 0
for _ in 0 .. bench_n {
goroutines.chan_send(bench_chan1, voidptr(&val), true)
goroutines.chan_recv(bench_chan2, voidptr(&val), true)
}
bench_sem.post()
}
fn pong_side(arg voidptr) {
mut val := 0
for _ in 0 .. bench_n {
goroutines.chan_recv(bench_chan1, voidptr(&val), true)
val++
goroutines.chan_send(bench_chan2, voidptr(&val), true)
}
}
fn bench_ping_pong(n int) {
bench_n = n
bench_chan1 = goroutines.chan_make(int(sizeof(int)), 1)
bench_chan2 = goroutines.chan_make(int(sizeof(int)), 1)
sw := time.new_stopwatch()
goroutines.goroutine_create(voidptr(&pong_side), unsafe { nil }, 0)
goroutines.goroutine_create(voidptr(&ping_side), unsafe { nil }, 0)
bench_sem.wait()
elapsed := sw.elapsed()
us := elapsed.microseconds()
ns_per := elapsed.nanoseconds() / n
C.printf(c'ping-pong %d round-trips: %lld us (%lld ns/round-trip)\n', n, us, ns_per)
}
// --- contended channel ---
fn producer(arg voidptr) {
c := unsafe { &goroutines.Chan(arg) }
mut val := 0
for _ in 0 .. bench_msgs_per {
val++
goroutines.chan_send(c, voidptr(&val), true)
}
}
fn consumer_fn(arg voidptr) {
c := unsafe { &goroutines.Chan(arg) }
mut recv_val := 0
for _ in 0 .. bench_n {
goroutines.chan_recv(c, voidptr(&recv_val), true)
}
bench_sem.post()
}
fn bench_contended_channel(num_producers int, msgs_per_producer int) {
total := num_producers * msgs_per_producer
bench_n = total
bench_msgs_per = msgs_per_producer
c := goroutines.chan_make(int(sizeof(int)), 64)
sw := time.new_stopwatch()
goroutines.goroutine_create(voidptr(&consumer_fn), voidptr(c), 0)
for _ in 0 .. num_producers {
goroutines.goroutine_create(voidptr(&producer), voidptr(c), 0)
}
bench_sem.wait()
elapsed := sw.elapsed()
us := elapsed.microseconds()
ns_per := elapsed.nanoseconds() / total
C.printf(c'contended chan %d producers x %d msgs: %lld us (%lld ns/msg)\n', num_producers,
msgs_per_producer, us, ns_per)
}
fn main() {
C.printf(c'=== V Goroutine Benchmark ===\n\n')
bench_fan_out_fan_in(10)
bench_fan_out_fan_in(50)
bench_fan_out_fan_in(100)
bench_fan_out_fan_in(500)
C.printf(c'\n')
bench_ping_pong(1000)
bench_ping_pong(10000)
bench_ping_pong(100000)
C.printf(c'\n')
bench_contended_channel(4, 1000)
bench_contended_channel(10, 1000)
C.printf(c'\n')
goroutines.shutdown()
}
@@ -0,0 +1,124 @@
// V spawn (OS threads) benchmark – for comparison with Go goroutines.
//
// Uses V's built-in `spawn` (OS threads) and `chan` (V1 channels).
// This measures V's current threading primitives vs Go's goroutines.
//
// Run: v -prod run spawn_benchmark.v
module main
import time
// ---------------------------------------------------------------------------
// Benchmark 1 – fan-out / fan-in
// ---------------------------------------------------------------------------
fn fan_worker(c chan int) {
c <- 1
}
fn bench_fan_out_fan_in(n int) {
c := chan int{cap: n}
sw := time.new_stopwatch()
for _ in 0 .. n {
spawn fan_worker(c)
}
for _ in 0 .. n {
_ = <-c
}
elapsed := sw.elapsed()
us := elapsed.microseconds()
ns_per := elapsed.nanoseconds() / n
println('fan-out/fan-in ${n} threads: ${us} us (${ns_per} ns/thread)')
}
// ---------------------------------------------------------------------------
// Benchmark 2 – channel ping-pong
// ---------------------------------------------------------------------------
fn pinger(c1 chan int, c2 chan int, n int) {
mut val := 0
for _ in 0 .. n {
val = <-c1
val++
c2 <- val
}
}
fn bench_ping_pong(n int) {
c1 := chan int{cap: 1}
c2 := chan int{cap: 1}
spawn pinger(c1, c2, n)
sw := time.new_stopwatch()
mut val := 0
for _ in 0 .. n {
c1 <- val
val = <-c2
}
elapsed := sw.elapsed()
us := elapsed.microseconds()
ns_per := elapsed.nanoseconds() / n
println('ping-pong ${n} round-trips: ${us} us (${ns_per} ns/round-trip)')
}
// ---------------------------------------------------------------------------
// Benchmark 3 – contended channel (many producers, one consumer)
// ---------------------------------------------------------------------------
fn producer(c chan int, count int) {
mut val := 0
for _ in 0 .. count {
val++
c <- val
}
}
fn bench_contended_channel(num_producers int, msgs_per_producer int) {
total := num_producers * msgs_per_producer
c := chan int{cap: 64}
sw := time.new_stopwatch()
for _ in 0 .. num_producers {
spawn producer(c, msgs_per_producer)
}
for _ in 0 .. total {
_ = <-c
}
elapsed := sw.elapsed()
us := elapsed.microseconds()
ns_per := elapsed.nanoseconds() / total
println('contended chan ${num_producers} producers x ${msgs_per_producer} msgs: ${us} us (${ns_per} ns/msg)')
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
fn main() {
println('=== V spawn (OS threads) Benchmark ===')
println('')
bench_fan_out_fan_in(1000)
bench_fan_out_fan_in(10000)
// Skip 100k - too many OS threads
println('')
bench_ping_pong(10000)
bench_ping_pong(100000)
println('')
bench_contended_channel(10, 1000)
bench_contended_channel(100, 1000)
println('')
}
+228
View File
@@ -0,0 +1,228 @@
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Go-style goroutine runtime for V.
// Implements the GMP (Goroutine-Machine-Processor) scheduling model
// translated from the Go runtime (src/runtime/proc.go, runtime2.go).
//
// Goroutine - goroutine: lightweight unit of execution with its own stack
// Machine - machine: OS thread that executes goroutines
// Processor - processor: logical processor that manages a local run queue
//
// Design doc: https://golang.org/s/go11sched
@[has_globals]
module goroutines
import sync
import runtime as _
// GoFn is the type for a goroutine function: takes a voidptr argument.
pub type GoFn = fn (voidptr)
// Default goroutine stack size.
// Go uses 8KB with growable stacks; V/C needs more since stacks are fixed.
const default_stack_size = 256 * 1024
// Maximum number of goroutines in a Processor's local run queue (matches Go's 256)
const local_queue_size = 256
// How often to check the global queue for fairness (matches Go's 61)
const global_queue_check_interval = 61
// GStatus represents goroutine states (matches Go's runtime2.go)
pub enum GStatus {
idle // just allocated, not yet initialized
runnable // on a run queue, not currently executing
running // executing user code on an Machine
waiting // blocked (channel, mutex, etc.)
dead // finished execution, available for reuse
copystack // stack is being moved (not used yet)
}
// PStatus represents processor states
pub enum PStatus {
idle // not being used, available on idle list
running // owned by an Machine and executing code
stopped // halted
dead // no longer used
}
// Goroutine represents a goroutine - the fundamental unit of concurrent execution.
// Translated from Go's `type g struct` in runtime2.go.
pub struct Goroutine {
pub mut:
id u64 // unique goroutine id
status GStatus = .idle // current state
stack voidptr // stack memory (allocated)
stack_size int = default_stack_size // stack allocation size
context Context // saved CPU context for switching (ucontext_t or similar)
fn_ptr voidptr // function to execute
fn_arg voidptr // argument to the function
sched_link &Goroutine = unsafe { nil } // linked list link for run queues
m &Machine = unsafe { nil } // current Machine executing this Goroutine (nil if not running)
parent_id u64 // goroutine id of creator
wait_reason string // if status==waiting, why
preempt bool // preemption signal
}
// Machine represents a machine (OS thread) that executes goroutines.
// Translated from Go's `type m struct` in runtime2.go.
pub struct Machine {
pub mut:
id i64
g0 &Goroutine = unsafe { nil } // goroutine with scheduling stack
curg &Goroutine = unsafe { nil } // current running goroutine
p &Processor = unsafe { nil } // attached processor (nil if not executing Go code)
spinning bool // looking for work
blocked bool // blocked on a note
park sync.Semaphore // for parking/unparking
sched_link &Machine = unsafe { nil } // linked list for idle Machine list
thread thread // underlying OS thread handle
}
// Processor represents a processor - a resource required to execute goroutines.
// Each Processor has a local run queue. Translated from Go's `type p struct` in runtime2.go.
pub struct Processor {
pub mut:
id i32
status PStatus = .idle
m &Machine = unsafe { nil } // back-link to associated Machine
sched_tick u32 // incremented on every scheduler call
// Local run queue - lock-free SPMC ring buffer (matches Go's design)
runq_head u32 // consumer index (atomic)
runq_tail u32 // producer index (atomic)
runq [local_queue_size]&Goroutine // circular buffer
runnext &Goroutine = unsafe { nil } // next Goroutine to run (fast path, like Go's runnext)
// Free Goroutine list for reuse
g_free GoroutineList
// ID cache to avoid contention on the global counter
goid_cache u64
goid_cache_end u64
link &Processor = unsafe { nil } // linked list for idle Processor list
}
// Sched is the global scheduler state.
// Translated from Go's `type schedt struct` in runtime2.go.
pub struct Sched {
pub mut:
goid_gen u64 // global goroutine ID generator (atomic)
mu SpinLock // protects idle lists, global queue (spinlock is ucontext-safe)
// Idle Machine's waiting for work
midle &Machine = unsafe { nil }
nmidle i32
// Idle Processor's
pidle &Processor = unsafe { nil }
npidle i32
// Global run queue
runq GoroutineQueue
nmspinning i32 // number of spinning Machine's (atomic)
// All Processor's (indexed by id)
allp []&Processor
// Total Machine count
mnext i64
maxmcount i32 = 10000
// Global Goroutine free list
g_free_mu SpinLock
g_free GoroutineList
g_free_count i32
// Shutdown
stopped bool
}
// GoroutineQueue is a simple linked-list queue of Goroutine's (matches Go's gQueue).
pub struct GoroutineQueue {
pub mut:
head &Goroutine = unsafe { nil }
tail &Goroutine = unsafe { nil }
size i32
}
// GoroutineList is a list of Goroutine's.
pub struct GoroutineList {
pub mut:
head &Goroutine = unsafe { nil }
count i32
}
fn (mut q GoroutineQueue) push_back(gp &Goroutine) {
mut g := unsafe { gp }
g.sched_link = unsafe { nil }
if q.tail != unsafe { nil } {
q.tail.sched_link = g
} else {
q.head = g
}
q.tail = g
q.size++
}
fn (mut q GoroutineQueue) push(gp &Goroutine) {
mut g := unsafe { gp }
g.sched_link = q.head
q.head = g
if q.tail == unsafe { nil } {
q.tail = g
}
q.size++
}
fn (mut q GoroutineQueue) pop() &Goroutine {
if q.head == unsafe { nil } {
return unsafe { nil }
}
gp := q.head
q.head = gp.sched_link
if q.head == unsafe { nil } {
q.tail = unsafe { nil }
}
q.size--
return gp
}
fn (q &GoroutineQueue) empty() bool {
return q.head == unsafe { nil }
}
fn (mut l GoroutineList) push(gp &Goroutine) {
mut g := unsafe { gp }
g.sched_link = l.head
l.head = g
l.count++
}
fn (mut l GoroutineList) pop() &Goroutine {
if l.head == unsafe { nil } {
return unsafe { nil }
}
gp := l.head
l.head = gp.sched_link
l.count--
return gp
}
fn (l &GoroutineList) empty() bool {
return l.head == unsafe { nil }
}
// Global scheduler instance
__global gsched = Sched{}
// Number of processors (defaults to number of CPU cores)
__global gomaxprocs = i32(0)
// All goroutines ever created (for debugging)
__global allgs_mu = SpinLock{}
__global allgs = []&Goroutine{}
+180
View File
@@ -0,0 +1,180 @@
// vtest build: !true // goroutines module is experimental; skip on all CIs for now
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module goroutines
import time
// Helper function for test_goroutine_create (must be a plain function, not a closure).
fn goroutine_test_worker(arg voidptr) {
// Signal that the goroutine ran by writing to the shared flag
if arg != unsafe { nil } {
unsafe {
mut flag := &bool(arg)
*flag = true
}
}
}
// Test basic goroutine creation
fn test_goroutine_create() {
mut done := false
goroutine_create(voidptr(goroutine_test_worker), voidptr(&done), 0)
// Give the goroutine time to run
time.sleep(100 * time.millisecond)
}
// Test channel make
fn test_chan_make() {
c := chan_make(int(sizeof(int)), 10)
assert c != unsafe { nil }
assert chan_cap(c) == 10
assert chan_len(c) == 0
}
// Test buffered channel send/recv
fn test_chan_buffered() {
c := chan_make(int(sizeof(int)), 5)
mut val := 42
assert chan_send(c, voidptr(&val), false) == true
assert chan_len(c) == 1
mut recv_val := 0
received, ok := chan_recv(c, voidptr(&recv_val), false)
assert received == true
assert ok == true
assert recv_val == 42
assert chan_len(c) == 0
}
// Test channel close
fn test_chan_close() {
c := chan_make(int(sizeof(int)), 1)
mut val := 100
chan_send(c, voidptr(&val), false)
chan_close(c)
// Should still receive buffered value
mut recv_val := 0
received, ok := chan_recv(c, voidptr(&recv_val), false)
assert received == true
// After close with data, ok should be true
}
// Test GoroutineQueue operations
fn test_gqueue() {
mut q := GoroutineQueue{}
assert q.empty()
assert q.size == 0
mut g1 := &Goroutine{
id: 1
}
mut g2 := &Goroutine{
id: 2
}
mut g3 := &Goroutine{
id: 3
}
q.push_back(g1)
assert !q.empty()
assert q.size == 1
q.push_back(g2)
q.push_back(g3)
assert q.size == 3
gp := q.pop()
assert gp.id == 1
assert q.size == 2
gp2 := q.pop()
assert gp2.id == 2
gp3 := q.pop()
assert gp3.id == 3
assert q.empty()
}
// Test GoroutineList operations
fn test_glist() {
mut l := GoroutineList{}
assert l.empty()
mut g1 := &Goroutine{
id: 10
}
mut g2 := &Goroutine{
id: 20
}
l.push(g1)
assert !l.empty()
assert l.count == 1
l.push(g2)
assert l.count == 2
// GoroutineList is a stack (LIFO)
gp := l.pop()
assert gp.id == 20
gp2 := l.pop()
assert gp2.id == 10
assert l.empty()
}
// Test WaitQ operations
fn test_waitq() {
mut q := WaitQ{}
assert q.empty()
mut g1 := &Goroutine{
id: 1
}
mut g2 := &Goroutine{
id: 2
}
mut s1 := &Sudog{
g: g1
}
mut s2 := &Sudog{
g: g2
}
q.enqueue(s1)
assert !q.empty()
q.enqueue(s2)
sg := q.dequeue()
assert sg.g.id == 1
sg2 := q.dequeue()
assert sg2.g.id == 2
assert q.empty()
}
// Test scheduler initialization
fn test_scheduler_init() {
// The scheduler should have been initialized by module init()
assert gomaxprocs > 0
assert gsched.allp.len > 0
assert gsched.allp.len == int(gomaxprocs)
// P0 should be running (attached to M0)
p0 := gsched.allp[0]
assert p0.status == .running
}
// Test goroutine ID allocation
fn test_goid_allocation() {
id1 := assign_goid()
id2 := assign_goid()
// IDs should be unique and increasing
assert id2 > id1
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef GOROUTINES_TLS_H
#define GOROUTINES_TLS_H
#include <stdint.h>
void *goroutines_get_current_m(void);
void goroutines_set_current_m(void *mp);
uint32_t goroutines_atomic_load_u32(volatile uint32_t *ptr);
void goroutines_atomic_store_u32(volatile uint32_t *ptr, uint32_t val);
uint32_t goroutines_atomic_fetch_add_u32(volatile uint32_t *ptr, uint32_t val);
int32_t goroutines_atomic_fetch_add_i32(volatile int32_t *ptr, int32_t val);
int32_t goroutines_atomic_fetch_sub_i32(volatile int32_t *ptr, int32_t val);
uint64_t goroutines_atomic_fetch_add_u64(volatile uint64_t *ptr, uint64_t val);
int goroutines_atomic_cas_u32(volatile uint32_t *ptr, uint32_t *expected, uint32_t desired);
int goroutines_atomic_cas_ptr(void *volatile *ptr, void **expected, void *desired);
void grt_spinlock_lock(volatile int32_t *lock);
void grt_spinlock_unlock(volatile int32_t *lock);
#endif
+111
View File
@@ -0,0 +1,111 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Goroutine runtime initialization.
// Translated from Go's schedinit() and procresize() in proc.go.
module goroutines
import runtime
// init initializes the goroutine scheduler.
// Creates P's (one per CPU core by default) and the initial M.
// Translated from Go's schedinit() + procresize().
fn init() {
// Determine number of processors
n := runtime.nr_cpus()
if n < 1 {
gomaxprocs = 1
} else if n > 256 {
gomaxprocs = 256
} else {
gomaxprocs = i32(n)
}
// Initialize the scheduler
gsched.maxmcount = 10000
gsched.mnext = 1
// Create all P's (translated from procresize in proc.go)
gsched.allp = []&Processor{cap: int(gomaxprocs)}
for i in 0 .. gomaxprocs {
mut pp := &Processor{
id: i
status: .idle
}
// Initialize the local run queue
for j in 0 .. local_queue_size {
pp.runq[j] = unsafe { nil }
}
gsched.allp << pp
}
// Create the initial M (M0) for the main thread
mut m0 := &Machine{
id: 0
g0: &Goroutine{
id: 0
status: .running
}
}
// Create a "main goroutine" so chan_send/recv work from the main thread
mut main_g := &Goroutine{
id: 0
status: .running
}
m0.curg = main_g
main_g.m = m0
// Wire M0 to P0
mut p0 := gsched.allp[0]
wire_p(mut m0, mut p0)
// Set the current thread's M
set_current_m(m0)
// Put remaining P's on the idle list
for i in 1 .. gomaxprocs {
pid_put(gsched.allp[i])
}
}
// set_max_procs changes the number of active processors.
// Returns the previous value. Translated from Go's GOMAXPROCS().
pub fn set_max_procs(n int) int {
old := int(gomaxprocs)
if n < 1 || n == old {
return old
}
// TODO: implement full procresize like Go does
// For now, just update the count
gomaxprocs = i32(n)
return old
}
// num_goroutine returns the number of goroutines that currently exist.
pub fn num_goroutine() int {
allgs_mu.acquire()
mut count := 0
for g in allgs {
if g.status != .dead {
count++
}
}
allgs_mu.release()
return count
}
// shutdown gracefully shuts down the goroutine scheduler.
pub fn shutdown() {
gsched.stopped = true
// Wake all idle M's so they can exit
gsched.mu.acquire()
mut mp := gsched.midle
for mp != unsafe { nil } {
mp.park.post()
mp = mp.sched_link
}
gsched.mu.release()
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Goroutine parking (blocking/unblocking) mechanism.
// Translated from Go's gopark/goready in proc.go.
//
// When a goroutine needs to block (e.g., waiting on a channel),
// it "parks" itself - yielding its M to the scheduler.
// When the condition is met, another goroutine "readies" it
// by putting it back on a run queue.
module goroutines
// gopark puts the current goroutine into a waiting state.
// The goroutine can be made runnable again by calling goready.
// reason describes why the goroutine is parking (for debugging).
// Translated from Go's gopark() in proc.go.
pub fn gopark(reason string) {
mut mp := get_current_m()
if mp == unsafe { nil } {
return
}
mut gp := mp.curg
if gp == unsafe { nil } {
return
}
gp.status = .waiting
gp.wait_reason = reason
// Dissociate G from M
mp.curg = unsafe { nil }
gp.m = unsafe { nil }
// Switch back to the scheduler (M's g0 context)
context_switch(mut &gp.context, &mp.g0.context)
}
// goready puts a waiting goroutine back on a run queue.
// Translated from Go's goready() in proc.go.
pub fn goready(gp &Goroutine) {
if gp == unsafe { nil } {
return
}
mut g := unsafe { gp }
if g.status != .waiting {
return
}
g.status = .runnable
g.wait_reason = ''
// Put it on the current P's local run queue, or global if no P
mut pp := get_current_p()
if pp != unsafe { nil } {
runq_put(mut pp, g, true)
} else {
glob_runq_put(g)
}
// Try to wake an idle P to run this goroutine
wake_p()
}
// gosched yields the processor, allowing other goroutines to run.
// Translated from Go's Gosched() / goschedImpl() in proc.go.
pub fn gosched() {
mut mp := get_current_m()
if mp == unsafe { nil } {
return
}
mut gp := mp.curg
if gp == unsafe { nil } {
return
}
mut pp := mp.p
if pp == unsafe { nil } {
return
}
// Put the current G back on the run queue as runnable
gp.status = .runnable
mp.curg = unsafe { nil }
gp.m = unsafe { nil }
// Put on local queue
runq_put(mut pp, gp, false)
// Switch back to scheduler
context_switch(mut &gp.context, &mp.g0.context)
}
// Sudog represents a G in a wait list (e.g., channel wait queue).
// Translated from Go's sudog struct in runtime2.go.
pub struct Sudog {
pub mut:
g &Goroutine = unsafe { nil } // the waiting goroutine
next &Sudog = unsafe { nil } // next in wait list
prev &Sudog = unsafe { nil } // prev in wait list
elem voidptr // data element (may point to stack)
success bool // true if woken by successful channel op
c voidptr // channel pointer
}
// WaitQ is a wait queue of Sudogs (used by channels).
// Translated from Go's waitq struct in runtime2.go.
pub struct WaitQ {
pub mut:
first &Sudog = unsafe { nil }
last &Sudog = unsafe { nil }
}
pub fn (mut q WaitQ) enqueue(s &Sudog) {
mut sg := unsafe { s }
sg.next = unsafe { nil }
sg.prev = q.last
if q.last != unsafe { nil } {
q.last.next = sg
} else {
q.first = sg
}
q.last = sg
}
pub fn (mut q WaitQ) dequeue() &Sudog {
sg := q.first
if sg == unsafe { nil } {
return unsafe { nil }
}
q.first = sg.next
if q.first != unsafe { nil } {
q.first.prev = unsafe { nil }
} else {
q.last = unsafe { nil }
}
mut s := unsafe { sg }
s.next = unsafe { nil }
s.prev = unsafe { nil }
return s
}
pub fn (q &WaitQ) empty() bool {
return q.first == unsafe { nil }
}
+642
View File
@@ -0,0 +1,642 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// GMP Scheduler - translated from Go's runtime/proc.go.
//
// The scheduler distributes ready-to-run goroutines over worker threads.
// Key functions translated from Go:
// - schedule() -> scheduler_loop()
// - findRunnable() -> find_runnable()
// - execute() -> execute()
// - newproc() -> goroutine_create()
// - wakep() -> wake_p()
// - runqput() -> runq_put()
// - runqget() -> runq_get()
// - stealWork() -> steal_work()
module goroutines
// import sync as _
// import runtime as _
// goroutine_create creates a new goroutine to run `f` with argument `arg`.
// This is the equivalent of Go's `newproc` function.
// Called by the compiler for `go expr()`.
pub fn goroutine_create(f voidptr, arg voidptr, arg_size int) {
gp := newproc1(f, arg, arg_size)
// Put on global run queue so any M can pick it up.
// This is simpler than local queue + work stealing and avoids
// visibility issues when the creator is the main (non-scheduler) thread.
glob_runq_put(gp)
// Try to wake an idle P if needed
wake_p()
}
// newproc1 allocates and initializes a new G.
// Translated from Go's newproc1 in proc.go.
fn newproc1(f voidptr, arg voidptr, arg_size int) &Goroutine {
// Try to get a dead G from the local P's free list first
mut pp := get_current_p()
mut gp := if pp != unsafe { nil } {
gfget(mut pp)
} else {
unsafe { &Goroutine(nil) }
}
if gp == unsafe { nil } {
// Try global free list
gp = gfget_global()
}
if gp == unsafe { nil } {
// Allocate a new G
gp = &Goroutine{}
}
// Allocate or reuse stack
if gp.stack == unsafe { nil } {
stack_size := default_stack_size
gp.stack = unsafe { malloc(stack_size) }
gp.stack_size = stack_size
}
gp.fn_ptr = f
gp.fn_arg = arg
gp.status = .runnable
gp.preempt = false
// Assign goroutine ID
gp.id = assign_goid()
// Initialize context to run goroutine_entry
context_init(mut &gp.context, gp.stack, gp.stack_size, goroutine_trampoline, voidptr(gp))
// Track all goroutines
allgs_mu.acquire()
allgs << gp
allgs_mu.release()
return gp
}
// goroutine_trampoline is the entry point for all goroutines.
// It calls the user function, then cleans up and returns to the scheduler.
// Equivalent to Go's goexit -> goexit0.
fn goroutine_trampoline(arg voidptr) {
mut gp := unsafe { &Goroutine(arg) }
// Call the actual function
if gp.fn_ptr != unsafe { nil } {
call_goroutine_fn(gp.fn_ptr, gp.fn_arg)
}
// Function returned - goroutine is done
goexit0(mut gp)
}
// call_goroutine_fn calls the goroutine's function pointer with the given argument.
fn call_goroutine_fn(fn_ptr voidptr, arg voidptr) {
unsafe {
cb := GoFn(fn_ptr)
cb(arg)
}
}
// goexit0 handles goroutine cleanup after the user function returns.
// Translated from Go's goexit0 in proc.go.
fn goexit0(mut gp Goroutine) {
mut mp := get_current_m()
gp.status = .dead
gp.m = unsafe { nil }
gp.fn_ptr = unsafe { nil }
gp.fn_arg = unsafe { nil }
gp.wait_reason = ''
gp.preempt = false
// Dissociate G from M
if mp != unsafe { nil } {
mp.curg = unsafe { nil }
}
// Put the dead G on the free list for reuse
mut pp := get_current_p()
if pp != unsafe { nil } {
gfput(mut pp, gp)
} else {
gfput_global(gp)
}
// Switch back to the scheduler (g0 context).
// This returns to schedule_loop via execute's context_switch.
if mp != unsafe { nil } && mp.g0 != unsafe { nil } {
context_set(&mp.g0.context)
}
}
// schedule is the main scheduler entry point.
// Finds a runnable goroutine and executes it. Never returns.
// Translated from Go's schedule() in proc.go.
pub fn schedule() {
mut mp := get_current_m()
if mp == unsafe { nil } {
return
}
mut pp := mp.p
if pp == unsafe { nil } {
return
}
pp.sched_tick++
// Find a runnable goroutine
gp, _ := find_runnable(mut mp, mut pp)
if gp == unsafe { nil } {
// No work found - park this M
park_m(mut mp)
return
}
// Execute the goroutine
execute(mut mp, mut pp, gp)
}
// find_runnable finds a runnable goroutine to execute.
// Tries: local queue, global queue, work stealing from other P's.
// Translated from Go's findRunnable() in proc.go.
fn find_runnable(mut mp Machine, mut pp Processor) (&Goroutine, bool) {
// Check global queue every Nth tick for fairness (Go uses 61)
if pp.sched_tick % global_queue_check_interval == 0 {
gp := glob_runq_get()
if gp != unsafe { nil } {
return gp, false
}
}
// Try local run queue first
gp, inherit := runq_get(mut pp)
if gp != unsafe { nil } {
return gp, inherit
}
// Try global run queue
gp2 := glob_runq_get()
if gp2 != unsafe { nil } {
return gp2, false
}
// Try to steal work from other P's
gp3 := steal_work(mut pp)
if gp3 != unsafe { nil } {
return gp3, false
}
return unsafe { nil }, false
}
// execute runs a goroutine on the current M.
// Translated from Go's execute() in proc.go.
fn execute(mut mp Machine, mut pp Processor, gp &Goroutine) {
mut g := unsafe { gp }
// Associate G with M
mp.curg = g
g.m = mp
g.status = .running
// Switch context to the goroutine
context_switch(mut &mp.g0.context, &g.context)
// When we return here, the goroutine has yielded back to us
// The scheduler loop will be re-entered
}
// wake_p tries to wake an idle P to run goroutines.
// Translated from Go's wakep() in proc.go.
fn wake_p() {
// Check if there's an idle P
gsched.mu.acquire()
if gsched.npidle == 0 {
gsched.mu.release()
return
}
// Don't wake if there are already spinning M's
if gsched.nmspinning > 0 {
gsched.mu.release()
return
}
// Get an idle P
pp := pid_get()
if pp == unsafe { nil } {
gsched.mu.release()
return
}
gsched.mu.release()
// Start a new M for this P (or wake an idle one)
start_m(pp)
}
// park_m parks the current M - it goes to sleep waiting for work.
fn park_m(mut mp Machine) {
// Release P and add M to idle list under lock
gsched.mu.acquire()
if mp.p != unsafe { nil } {
pid_put(mp.p)
mp.p = unsafe { nil }
}
mp.sched_link = gsched.midle
gsched.midle = mp
gsched.nmidle++
gsched.mu.release()
// Sleep until woken
mp.park.wait()
// Woken up - acquire a P and return to schedule_loop
acquire_p(mut mp)
}
// acquire_p tries to get an idle P for the given M.
fn acquire_p(mut mp Machine) {
gsched.mu.acquire()
mut pp := pid_get()
gsched.mu.release()
if pp != unsafe { nil } {
wire_p(mut mp, mut pp)
}
}
// wire_p associates a P with an M.
fn wire_p(mut mp Machine, mut pp Processor) {
mp.p = pp
pp.m = mp
pp.status = .running
}
// start_m starts or wakes an M to run the given P.
// Translated from Go's startm() in proc.go.
fn start_m(pp &Processor) {
gsched.mu.acquire()
// Try to get an idle M first
mut mp := gsched.midle
if mp != unsafe { nil } {
gsched.midle = mp.sched_link
gsched.nmidle--
gsched.mu.release()
// Give it the P and wake it
mut p := unsafe { pp }
wire_p(mut mp, mut p)
mp.park.post()
return
}
// No idle M available - create a new one
id := gsched.mnext
gsched.mnext++
gsched.mu.release()
new_m(id, pp)
}
// new_m creates a new OS thread (M) and associates it with a P.
fn new_m(id i64, pp &Processor) {
mut mp := &Machine{
id: id
g0: &Goroutine{
status: .running
}
}
mut p := unsafe { pp }
wire_p(mut mp, mut p)
mp.thread = spawn m_thread_entry(mut mp)
}
// m_thread_entry is the entry point for new M (OS thread) goroutine scheduling loops.
fn m_thread_entry(mut mp Machine) {
// Register this M as the current thread's Machine so that
// get_current_m()/get_current_p() work on worker threads.
set_current_m(mp)
// Enter the scheduling loop - this never returns
schedule_loop(mut mp)
}
// schedule_loop is the main loop for an M. It repeatedly finds and runs goroutines.
fn schedule_loop(mut mp Machine) {
for {
if gsched.stopped {
return
}
mut pp := mp.p
if pp == unsafe { nil } {
acquire_p(mut mp)
pp = mp.p
if pp == unsafe { nil } {
// No P available, park
park_m(mut mp)
continue
}
}
pp.sched_tick++
gp, _ := find_runnable(mut mp, mut pp)
if gp == unsafe { nil } {
// No work - try spinning briefly before parking
if !mp.spinning {
mp.spinning = true
C.goroutines_atomic_fetch_add_i32(&gsched.nmspinning, 1)
}
// Spin a bit
mut found := false
for _ in 0 .. 20 {
gp2, _ := find_runnable(mut mp, mut pp)
if gp2 != unsafe { nil } {
mp.spinning = false
C.goroutines_atomic_fetch_sub_i32(&gsched.nmspinning, 1)
execute(mut mp, mut pp, gp2)
found = true
break
}
// Yield to avoid burning CPU
proc_yield(10)
}
if !found {
if mp.spinning {
mp.spinning = false
C.goroutines_atomic_fetch_sub_i32(&gsched.nmspinning, 1)
}
park_m(mut mp)
}
continue
}
if mp.spinning {
mp.spinning = false
C.goroutines_atomic_fetch_sub_i32(&gsched.nmspinning, 1)
}
execute(mut mp, mut pp, gp)
}
}
// steal_work tries to steal goroutines from other P's run queues.
// Translated from Go's stealWork() in proc.go.
fn steal_work(mut thisp Processor) &Goroutine {
n := gsched.allp.len
if n <= 1 {
return unsafe { nil }
}
// Randomize starting point to avoid contention
start := u32(C.rand()) % u32(n)
for i := u32(0); i < u32(n); i++ {
idx := (start + i) % u32(n)
pp := gsched.allp[idx]
if pp == thisp {
continue
}
// Try to steal half of the target's run queue
mut target := unsafe { pp }
gp := runq_steal(mut target, mut thisp)
if gp != unsafe { nil } {
return gp
}
}
return unsafe { nil }
}
// runq_steal steals half of pp's local run queue.
// Translated from Go's runqgrab/runqsteal in proc.go.
fn runq_steal(mut pp Processor, mut thisp Processor) &Goroutine {
t := C.goroutines_atomic_load_u32(&pp.runq_tail)
h := C.goroutines_atomic_load_u32(&pp.runq_head)
n := t - h
if n == 0 {
// Try runnext
next := pp.runnext
if next != unsafe { nil } {
if C.goroutines_atomic_cas_ptr(voidptr(&pp.runnext), voidptr(&next), unsafe { nil }) {
return next
}
}
return unsafe { nil }
}
// Steal half
steal := n - n / 2
mut first := unsafe { &Goroutine(nil) }
for i := u32(0); i < steal; i++ {
mut gp := pp.runq[(h + i) % local_queue_size]
if i == 0 {
first = gp
} else {
// Enqueue remaining stolen goroutines into thief's local run queue
runq_put(mut thisp, gp, false)
}
}
C.goroutines_atomic_fetch_add_u32(&pp.runq_head, steal)
return first
}
// Global run queue operations (translated from Go's globrunqput/get)
fn glob_runq_put(gp &Goroutine) {
gsched.mu.acquire()
gsched.runq.push_back(gp)
gsched.mu.release()
}
fn glob_runq_get() &Goroutine {
gsched.mu.acquire()
gp := gsched.runq.pop()
gsched.mu.release()
return gp
}
// Local run queue operations (translated from Go's runqput/runqget)
// runq_put puts a G on the local run queue.
// If next is true, it goes into runnext for immediate scheduling.
// Translated from Go's runqput() in proc.go.
fn runq_put(mut pp Processor, gp &Goroutine, next bool) {
if next {
// Fast path: put as runnext
old := pp.runnext
pp.runnext = unsafe { gp }
if old == unsafe { nil } {
return
}
// Kick old runnext to the regular queue
runq_put(mut pp, old, false)
return
}
// Regular path: put on the ring buffer
h := C.goroutines_atomic_load_u32(&pp.runq_head)
t := pp.runq_tail
if t - h < local_queue_size {
pp.runq[t % local_queue_size] = unsafe { gp }
C.goroutines_atomic_store_u32(&pp.runq_tail, t + 1)
return
}
// Queue is full - put half on global queue
runq_put_slow(mut pp, gp, h, t)
}
// runq_put_slow moves half the local queue to the global queue.
// Translated from Go's runqputslow() in proc.go.
fn runq_put_slow(mut pp Processor, gp &Goroutine, h u32, t u32) {
n := (t - h) / 2
mut batch := GoroutineQueue{}
for i := u32(0); i < n; i++ {
g := pp.runq[(h + i) % local_queue_size]
batch.push_back(g)
}
C.goroutines_atomic_fetch_add_u32(&pp.runq_head, n)
batch.push_back(gp)
gsched.mu.acquire()
for !batch.empty() {
gsched.runq.push_back(batch.pop())
}
gsched.mu.release()
}
// runq_get gets a G from the local run queue.
// Translated from Go's runqget() in proc.go.
fn runq_get(mut pp Processor) (&Goroutine, bool) {
// Check runnext first (fast path)
next := pp.runnext
if next != unsafe { nil } {
pp.runnext = unsafe { nil }
return next, true
}
// Regular queue
for {
h := C.goroutines_atomic_load_u32(&pp.runq_head)
t := pp.runq_tail
if t == h {
return unsafe { nil }, false
}
gp := pp.runq[h % local_queue_size]
if C.goroutines_atomic_cas_u32(&pp.runq_head, &h, h + 1) {
return gp, false
}
}
return unsafe { nil }, false
}
// Idle P list operations
fn pid_get() &Processor {
pp := gsched.pidle
if pp != unsafe { nil } {
gsched.pidle = pp.link
gsched.npidle--
mut p := unsafe { pp }
p.status = .running
p.link = unsafe { nil }
}
return pp
}
fn pid_put(pp &Processor) {
mut p := unsafe { pp }
p.status = .idle
p.m = unsafe { nil }
p.link = gsched.pidle
gsched.pidle = p
gsched.npidle++
}
// G free list operations (translated from Go's gfput/gfget)
fn gfput(mut pp Processor, gp &Goroutine) {
mut g := unsafe { gp }
g.status = .dead
g.fn_ptr = unsafe { nil }
g.fn_arg = unsafe { nil }
g.sched_link = unsafe { nil }
pp.g_free.push(g)
}
fn gfget(mut pp Processor) &Goroutine {
return pp.g_free.pop()
}
fn gfput_global(gp &Goroutine) {
gsched.g_free_mu.acquire()
gsched.g_free.push(unsafe { gp })
gsched.g_free_count++
gsched.g_free_mu.release()
}
fn gfget_global() &Goroutine {
gsched.g_free_mu.acquire()
gp := gsched.g_free.pop()
if gp != unsafe { nil } {
gsched.g_free_count--
}
gsched.g_free_mu.release()
return gp
}
// assign_goid allocates a unique goroutine ID.
// Uses per-P caching to avoid contention (like Go's goidcache).
fn assign_goid() u64 {
mut pp := get_current_p()
if pp != unsafe { nil } && pp.goid_cache < pp.goid_cache_end {
id := pp.goid_cache
unsafe {
pp.goid_cache++
}
return id
}
// Refill cache from global counter
batch := u64(16)
id := C.goroutines_atomic_fetch_add_u64(&gsched.goid_gen, batch)
if pp != unsafe { nil } {
unsafe {
pp.goid_cache = id + 1
pp.goid_cache_end = id + batch
}
}
return id
}
// proc_yield spins for a short time (used during work stealing).
fn proc_yield(count int) {
for _ in 0 .. count {
// CPU pause instruction to reduce power and contention
$if amd64 {
asm volatile amd64 {
pause
}
}
$if arm64 {
asm volatile arm64 {
yield
}
}
}
}
// get_current_m returns the M for the current OS thread.
// Uses thread-local storage via C _Thread_local (see tls.c).
fn get_current_m() &Machine {
return unsafe { &Machine(C.goroutines_get_current_m()) }
}
fn set_current_m(mp &Machine) {
C.goroutines_set_current_m(voidptr(mp))
}
// get_current_p returns the P for the current OS thread's M.
fn get_current_p() &Processor {
mp := get_current_m()
if mp == unsafe { nil } {
return unsafe { nil }
}
return mp.p
}
// get_current_g returns the currently running G.
pub fn get_current_g() &Goroutine {
mp := get_current_m()
if mp == unsafe { nil } {
return unsafe { nil }
}
return mp.curg
}
+128
View File
@@ -0,0 +1,128 @@
// Thread-local storage, atomic operations, and spinlock for goroutines scheduler.
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32) || defined(_WIN64)
#include <windows.h>
#else
#include <stdatomic.h>
#include <sched.h>
#endif
// Thread-local Machine pointer
#if defined(_WIN32) || defined(_WIN64)
static __declspec(thread) void *_goroutines_current_m = NULL;
#else
static _Thread_local void *_goroutines_current_m = NULL;
#endif
void *goroutines_get_current_m(void) {
return _goroutines_current_m;
}
void goroutines_set_current_m(void *mp) {
_goroutines_current_m = mp;
}
#if defined(_WIN32) || defined(_WIN64)
// Windows atomic operations using MSVC intrinsics / WinAPI
uint32_t goroutines_atomic_load_u32(volatile uint32_t *ptr) {
return InterlockedCompareExchange((volatile LONG *)ptr, 0, 0);
}
void goroutines_atomic_store_u32(volatile uint32_t *ptr, uint32_t val) {
InterlockedExchange((volatile LONG *)ptr, (LONG)val);
}
uint32_t goroutines_atomic_fetch_add_u32(volatile uint32_t *ptr, uint32_t val) {
return (uint32_t)InterlockedExchangeAdd((volatile LONG *)ptr, (LONG)val);
}
int32_t goroutines_atomic_fetch_add_i32(volatile int32_t *ptr, int32_t val) {
return (int32_t)InterlockedExchangeAdd((volatile LONG *)ptr, (LONG)val);
}
int32_t goroutines_atomic_fetch_sub_i32(volatile int32_t *ptr, int32_t val) {
return (int32_t)InterlockedExchangeAdd((volatile LONG *)ptr, (LONG)(-val));
}
uint64_t goroutines_atomic_fetch_add_u64(volatile uint64_t *ptr, uint64_t val) {
return (uint64_t)InterlockedExchangeAdd64((volatile LONG64 *)ptr, (LONG64)val);
}
int goroutines_atomic_cas_u32(volatile uint32_t *ptr, uint32_t *expected, uint32_t desired) {
uint32_t old = (uint32_t)InterlockedCompareExchange((volatile LONG *)ptr, (LONG)desired, (LONG)*expected);
if (old == *expected) return 1;
*expected = old;
return 0;
}
int goroutines_atomic_cas_ptr(void *volatile *ptr, void **expected, void *desired) {
void *old = InterlockedCompareExchangePointer(ptr, desired, *expected);
if (old == *expected) return 1;
*expected = old;
return 0;
}
void grt_spinlock_lock(volatile int32_t *lock) {
while (InterlockedExchange((volatile LONG *)lock, 1) != 0) {
YieldProcessor();
}
}
void grt_spinlock_unlock(volatile int32_t *lock) {
InterlockedExchange((volatile LONG *)lock, 0);
}
#else
// POSIX atomic operations using C11 stdatomic
uint32_t goroutines_atomic_load_u32(volatile uint32_t *ptr) {
return atomic_load((_Atomic uint32_t *)ptr);
}
void goroutines_atomic_store_u32(volatile uint32_t *ptr, uint32_t val) {
atomic_store((_Atomic uint32_t *)ptr, val);
}
uint32_t goroutines_atomic_fetch_add_u32(volatile uint32_t *ptr, uint32_t val) {
return atomic_fetch_add((_Atomic uint32_t *)ptr, val);
}
int32_t goroutines_atomic_fetch_add_i32(volatile int32_t *ptr, int32_t val) {
return atomic_fetch_add((_Atomic int32_t *)ptr, val);
}
int32_t goroutines_atomic_fetch_sub_i32(volatile int32_t *ptr, int32_t val) {
return atomic_fetch_sub((_Atomic int32_t *)ptr, val);
}
uint64_t goroutines_atomic_fetch_add_u64(volatile uint64_t *ptr, uint64_t val) {
return atomic_fetch_add((_Atomic uint64_t *)ptr, val);
}
int goroutines_atomic_cas_u32(volatile uint32_t *ptr, uint32_t *expected, uint32_t desired) {
return atomic_compare_exchange_strong((_Atomic uint32_t *)ptr, expected, desired);
}
int goroutines_atomic_cas_ptr(void *volatile *ptr, void **expected, void *desired) {
return atomic_compare_exchange_strong((_Atomic(void *) *)ptr, expected, desired);
}
void grt_spinlock_lock(volatile int32_t *lock) {
while (atomic_exchange((_Atomic int32_t *)lock, 1) != 0) {
// Spin with pause hint
#if defined(__x86_64__) || defined(__i386__)
__asm__ volatile("pause");
#elif defined(__aarch64__)
__asm__ volatile("yield");
#endif
}
}
void grt_spinlock_unlock(volatile int32_t *lock) {
atomic_store((_Atomic int32_t *)lock, 0);
}
#endif