runtime: map every goroutine to a new OS thread

This is not a scheduler in the runtime, instead every goroutine is
mapped to a single OS thread - meaning 1:1 scheduling.

While this may not perform well (or at all) for large numbers of
threads, it greatly simplifies many things in the runtime. For example,
blocking syscalls can be called directly instead of having to use epoll
or similar. Also, we don't need to do anything special to call C code -
the default stack is all we need.
This commit is contained in:
Ayke van Laethem
2024-10-24 10:26:17 +02:00
committed by Ron Evans
parent 193f91b870
commit 120d17c124
17 changed files with 653 additions and 13 deletions
+39 -6
View File
@@ -1,5 +1,21 @@
//go:build gc.boehm
// This is the Boehm-Demers-Weiser conservative garbage collector, integrated
// into TinyGo.
//
// Note that we use a special way of dealing with threads:
// * All calls to the bdwgc library are serialized using locks.
// * When the bdwgc library wants to push GC roots, all other threads that are
// running are stopped.
// * After returning from a bdwgc library call, the caller checks whether
// other threads were stopped (meaning a GC cycle happened) and resumes the
// world.
// This is not exactly the most efficient way to do this. We can likely speed
// things up by using bdwgc-native wrappers for starting/stopping threads (and
// also to resume the world while sweeping). Also, thread local allocation might
// help. But we don't do any of these right now, it is left as a possible future
// improvement.
package runtime
import (
@@ -16,6 +32,10 @@ var zeroSizedAlloc uint8
var gcLock task.PMutex
// Normally false, set to true during a GC scan when all other threads get
// paused.
var needsResumeWorld bool
func initHeap() {
libgc_init()
@@ -25,13 +45,16 @@ func initHeap() {
var gcCallbackPtr = reflectlite.ValueOf(gcCallback).UnsafePointer()
func gcCallback() {
// Mark the system stack and (if we're on a goroutine stack) also the
// current goroutine stack.
markStack()
// Mark globals and all stacks, and stop the world if we're using threading.
gcMarkReachable()
findGlobals(func(start, end uintptr) {
libgc_push_all(start, end)
})
if needsResumeWorld {
// Should never happen, check for it anyway.
runtimePanic("gc: world already stopped")
}
// Note that we need to resume the world after finishing the GC call.
needsResumeWorld = true
}
func markRoots(start, end uintptr) {
@@ -57,6 +80,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
}
gcLock.Lock()
needsResumeWorld = false
var ptr unsafe.Pointer
if layout == gclayout.NoPtrs {
// This object is entirely pointer free, for example make([]int, ...).
@@ -73,6 +97,9 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
// Memory returned from libgc_malloc has already been zeroed, so nothing
// to do here.
}
if needsResumeWorld {
gcResumeWorld()
}
gcLock.Unlock()
if ptr == nil {
runtimePanic("gc: out of memory")
@@ -86,7 +113,13 @@ func free(ptr unsafe.Pointer) {
}
func GC() {
gcLock.Lock()
needsResumeWorld = false
libgc_gcollect()
if needsResumeWorld {
gcResumeWorld()
}
gcLock.Unlock()
}
// This should be stack-allocated, but we don't currently have a good way of