mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
runtime: make Goexit exit host threads
The threads scheduler handled runtime.Goexit using the generic deadlock path, which parked the current pthread forever. Add a scheduler-specific goexit hook so it can remove the current task and exit the pthread while ordinary blocking deadlocks still block. Track main goroutine Goexit so the last remaining goroutine reports the expected deadlock instead of letting the process exit successfully. Expand the crash coverage with the Goexit cases covered by Big Go's runtime tests.
This commit is contained in:
@@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
|
||||
// This is a small number of very diverse targets that we want to test.
|
||||
tests := []sizeTest{
|
||||
// microcontrollers
|
||||
{"hifive1b", "examples/echo", 3699, 297, 0, 2252},
|
||||
{"microbit", "examples/serial", 2736, 356, 8, 2248},
|
||||
{"wioterminal", "examples/pininterrupt", 7960, 1652, 132, 7480},
|
||||
{"hifive1b", "examples/echo", 3765, 307, 0, 2260},
|
||||
{"microbit", "examples/serial", 2824, 368, 8, 2256},
|
||||
{"wioterminal", "examples/pininterrupt", 8041, 1663, 132, 7488},
|
||||
|
||||
// TODO: also check wasm. Right now this is difficult, because
|
||||
// wasm binaries are run through wasm-opt and therefore the
|
||||
|
||||
+30
-14
@@ -963,20 +963,36 @@ func TestGoexitCrash(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
output := &bytes.Buffer{}
|
||||
_, err = buildAndRun("testdata/goexit.go", config, output, nil, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
data, err := cmd.CombinedOutput()
|
||||
output.Write(data)
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("program unexpectedly exited successfully")
|
||||
}
|
||||
const want = "panic: panic after Goexit"
|
||||
if !strings.Contains(output.String(), want) {
|
||||
t.Fatalf("output does not contain %q:\n%s", want, output.String())
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"main", "all goroutines are asleep - deadlock!"},
|
||||
{"deadlock", "all goroutines are asleep - deadlock!"},
|
||||
{"exit", "all goroutines are asleep - deadlock!"},
|
||||
{"main-other", "all goroutines are asleep - deadlock!"},
|
||||
{"in-panic", "all goroutines are asleep - deadlock!"},
|
||||
{"panic", "panic: panic after Goexit"},
|
||||
{"recovered-panic", "all goroutines are asleep - deadlock!"},
|
||||
{"recover-before-panic", "all goroutines are asleep - deadlock!"},
|
||||
{"recover-before-panic-loop", "all goroutines are asleep - deadlock!"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
output := &bytes.Buffer{}
|
||||
_, err = buildAndRun("testdata/goexit.go", config, output, []string{tc.name}, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
data, err := cmd.CombinedOutput()
|
||||
output.Write(data)
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("program unexpectedly exited successfully")
|
||||
}
|
||||
if !strings.Contains(output.String(), tc.want) {
|
||||
t.Fatalf("output does not contain %q:\n%s", tc.want, output.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ type stackState struct {
|
||||
// The new goroutine is immediately started.
|
||||
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
|
||||
t := &Task{}
|
||||
addLiveTask(t)
|
||||
t.state.initialize(fn, args, stackSize)
|
||||
scheduleTask(t)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//go:build scheduler.tasks || scheduler.asyncify || scheduler.cores
|
||||
|
||||
package task
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
var (
|
||||
mainTask *Task
|
||||
liveTasks uint32
|
||||
mainExitedByGoexit uint32
|
||||
)
|
||||
|
||||
func addLiveTask(t *Task) {
|
||||
if mainTask == nil {
|
||||
mainTask = t
|
||||
}
|
||||
atomic.AddUint32(&liveTasks, 1)
|
||||
}
|
||||
|
||||
// Exit exits the current task because runtime.Goexit was called.
|
||||
func Exit() {
|
||||
exit(true)
|
||||
}
|
||||
|
||||
func exit(goexit bool) {
|
||||
t := Current()
|
||||
remaining := atomic.AddUint32(&liveTasks, ^uint32(0))
|
||||
if t == mainTask {
|
||||
if goexit {
|
||||
if remaining == 0 {
|
||||
runtimePanic("all goroutines are asleep - deadlock!")
|
||||
}
|
||||
atomic.StoreUint32(&mainExitedByGoexit, 1)
|
||||
}
|
||||
} else if atomic.LoadUint32(&mainExitedByGoexit) != 0 && remaining == 0 {
|
||||
runtimePanic("all goroutines are asleep - deadlock!")
|
||||
}
|
||||
|
||||
// TODO: explicitly free the stack after switching back to the scheduler.
|
||||
Pause()
|
||||
runtimePanic("unreachable")
|
||||
}
|
||||
@@ -33,8 +33,7 @@ type state struct {
|
||||
|
||||
//export tinygo_task_exit
|
||||
func taskExit() {
|
||||
// TODO: explicitly free the stack after switching back to the scheduler.
|
||||
Pause()
|
||||
exit(false)
|
||||
}
|
||||
|
||||
// initialize the state and prepare to call the specified function with the specified argument bundle.
|
||||
@@ -73,6 +72,7 @@ var startTask [0]uint8
|
||||
// The new goroutine is scheduled to run later.
|
||||
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
|
||||
t := &Task{}
|
||||
addLiveTask(t)
|
||||
t.state.initialize(fn, args, stackSize)
|
||||
scheduleTask(t)
|
||||
}
|
||||
|
||||
@@ -145,6 +145,11 @@ void* tinygo_task_current(void) {
|
||||
return current_task;
|
||||
}
|
||||
|
||||
// Exit the current thread.
|
||||
void tinygo_task_exit(void) {
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
// Send a signal to cause the task to pause for the GC mark phase.
|
||||
void tinygo_task_send_gc_signal(pthread_t thread) {
|
||||
pthread_kill(thread, taskPauseSignal);
|
||||
|
||||
@@ -43,7 +43,9 @@ var mainTask Task
|
||||
|
||||
// Queue of tasks (see QueueNext) that currently exist in the program.
|
||||
var activeTasks = &mainTask
|
||||
var activeTaskCount uint32 = 1
|
||||
var activeTaskLock PMutex
|
||||
var mainExitedByGoexit bool
|
||||
|
||||
func OnSystemStack() bool {
|
||||
runtimePanic("todo: task.OnSystemStack")
|
||||
@@ -95,9 +97,6 @@ func (t *Task) Resume() {
|
||||
t.state.pauseSem.Post()
|
||||
}
|
||||
|
||||
// otherGoroutines is the total number of live goroutines minus one.
|
||||
var otherGoroutines uint32
|
||||
|
||||
// Start a new OS thread.
|
||||
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
|
||||
t := &Task{}
|
||||
@@ -117,7 +116,7 @@ func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
|
||||
}
|
||||
t.state.QueueNext = activeTasks
|
||||
activeTasks = t
|
||||
otherGoroutines++
|
||||
activeTaskCount++
|
||||
activeTaskLock.Unlock()
|
||||
}
|
||||
|
||||
@@ -127,6 +126,12 @@ func taskExited(t *Task) {
|
||||
println("*** exit:", t.state.id)
|
||||
}
|
||||
|
||||
if exit(t) {
|
||||
runtimePanic("all goroutines are asleep - deadlock!")
|
||||
}
|
||||
}
|
||||
|
||||
func exit(t *Task) bool {
|
||||
// Remove from the queue.
|
||||
// TODO: this can be made more efficient by using a doubly linked list.
|
||||
activeTaskLock.Lock()
|
||||
@@ -135,16 +140,46 @@ func taskExited(t *Task) {
|
||||
if *q == t {
|
||||
*q = t.state.QueueNext
|
||||
found = true
|
||||
activeTaskCount--
|
||||
break
|
||||
}
|
||||
}
|
||||
otherGoroutines--
|
||||
deadlocked := mainExitedByGoexit && activeTaskCount == 0
|
||||
activeTaskLock.Unlock()
|
||||
|
||||
// Sanity check.
|
||||
if !found {
|
||||
runtimePanic("taskExited failed")
|
||||
}
|
||||
return deadlocked
|
||||
}
|
||||
|
||||
func otherTasks(current *Task) uint32 {
|
||||
if activeTaskCount == 0 {
|
||||
return 0
|
||||
}
|
||||
return activeTaskCount - 1
|
||||
}
|
||||
|
||||
// Exit exits the current task. If this is the main task and there are no other
|
||||
// goroutines, it reports a deadlock.
|
||||
func Exit() {
|
||||
t := Current()
|
||||
if t == &mainTask {
|
||||
activeTaskLock.Lock()
|
||||
noOtherTasks := otherTasks(t) == 0
|
||||
if !noOtherTasks {
|
||||
mainExitedByGoexit = true
|
||||
}
|
||||
activeTaskLock.Unlock()
|
||||
if noOtherTasks {
|
||||
runtimePanic("all goroutines are asleep - deadlock!")
|
||||
}
|
||||
}
|
||||
if exit(t) {
|
||||
runtimePanic("all goroutines are asleep - deadlock!")
|
||||
}
|
||||
tinygo_task_exit()
|
||||
}
|
||||
|
||||
// scanWaitGroup is used to wait on until all threads have finished the current state transition.
|
||||
@@ -206,7 +241,7 @@ func GCStopWorldAndScan() {
|
||||
gcState.Store(gcStateStopped)
|
||||
|
||||
// Set the number of threads to wait for.
|
||||
scanWaitGroup = initWaitGroup(otherGoroutines)
|
||||
scanWaitGroup = initWaitGroup(otherTasks(current))
|
||||
|
||||
// Pause all other threads.
|
||||
for t := activeTasks; t != nil; t = t.state.QueueNext {
|
||||
@@ -242,7 +277,7 @@ func GCResumeWorld() {
|
||||
}
|
||||
|
||||
// Set the wait group to track resume progress.
|
||||
scanWaitGroup = initWaitGroup(otherGoroutines)
|
||||
scanWaitGroup = initWaitGroup(otherTasks(Current()))
|
||||
|
||||
// Set the state to resumed.
|
||||
gcState.Store(gcStateResumed)
|
||||
@@ -304,6 +339,9 @@ func tinygo_task_init(t *Task, thread *threadID, numCPU *int32)
|
||||
//go:linkname tinygo_task_start tinygo_task_start
|
||||
func tinygo_task_start(fn uintptr, args unsafe.Pointer, t *Task, thread *threadID, stackTop *uintptr, stackSize uintptr) int32
|
||||
|
||||
//go:linkname tinygo_task_exit tinygo_task_exit
|
||||
func tinygo_task_exit()
|
||||
|
||||
// Pause the thread by sending it a signal.
|
||||
//
|
||||
//export tinygo_task_send_gc_signal
|
||||
|
||||
@@ -78,7 +78,7 @@ func panicOrGoexit(message interface{}, panicking panicState) {
|
||||
if panicking == panicGoexit {
|
||||
// Call to Goexit() instead of a panic.
|
||||
// Exit the goroutine instead of printing a panic message.
|
||||
deadlock()
|
||||
goexit()
|
||||
}
|
||||
printstring("panic: ")
|
||||
printitf(message)
|
||||
|
||||
@@ -2,9 +2,6 @@ package runtime
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//export abort
|
||||
func abort()
|
||||
|
||||
//export exit
|
||||
func libc_exit(code int)
|
||||
|
||||
@@ -132,6 +129,11 @@ func putchar(c byte) {
|
||||
libc_putchar(int(c))
|
||||
}
|
||||
|
||||
func abort() {
|
||||
libc_exit(1)
|
||||
trap()
|
||||
}
|
||||
|
||||
var heapSize uintptr = 128 * 1024 // small amount to start
|
||||
var heapMaxSize uintptr
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ func deadlock() {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func goexit() {
|
||||
task.Exit()
|
||||
}
|
||||
|
||||
// Add this task to the end of the run queue.
|
||||
func scheduleTask(t *task.Task) {
|
||||
runqueue.Push(t)
|
||||
|
||||
@@ -32,6 +32,10 @@ func deadlock() {
|
||||
trap()
|
||||
}
|
||||
|
||||
func goexit() {
|
||||
task.Exit()
|
||||
}
|
||||
|
||||
// Mark the given task as ready to resume.
|
||||
// This is allowed even if the task isn't paused yet, but will pause soon.
|
||||
func scheduleTask(t *task.Task) {
|
||||
|
||||
@@ -42,6 +42,10 @@ func deadlock() {
|
||||
runtimePanic("all goroutines are asleep - deadlock!")
|
||||
}
|
||||
|
||||
func goexit() {
|
||||
deadlock()
|
||||
}
|
||||
|
||||
func scheduleTask(t *task.Task) {
|
||||
// Pause() will panic, so this should not be reachable.
|
||||
}
|
||||
|
||||
@@ -40,10 +40,13 @@ func sleep(duration int64) {
|
||||
}
|
||||
|
||||
func deadlock() {
|
||||
// TODO: exit the thread via pthread_exit.
|
||||
task.Pause()
|
||||
}
|
||||
|
||||
func goexit() {
|
||||
task.Exit()
|
||||
}
|
||||
|
||||
func scheduleTask(t *task.Task) {
|
||||
t.Resume()
|
||||
}
|
||||
|
||||
Vendored
+75
-5
@@ -1,10 +1,80 @@
|
||||
package main
|
||||
|
||||
import "runtime"
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
panic("panic after Goexit")
|
||||
}()
|
||||
runtime.Goexit()
|
||||
switch os.Args[1] {
|
||||
case "main":
|
||||
runtime.Goexit()
|
||||
case "deadlock":
|
||||
f := func() {
|
||||
for i := 0; i < 10; i++ {
|
||||
}
|
||||
}
|
||||
go f()
|
||||
go f()
|
||||
runtime.Goexit()
|
||||
case "exit":
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond)
|
||||
}()
|
||||
i := 0
|
||||
runtime.SetFinalizer(&i, func(p *int) {})
|
||||
runtime.GC()
|
||||
runtime.Goexit()
|
||||
case "main-other":
|
||||
go func() {
|
||||
println("other goroutine ran")
|
||||
}()
|
||||
runtime.Goexit()
|
||||
case "in-panic":
|
||||
go func() {
|
||||
defer func() {
|
||||
runtime.Goexit()
|
||||
}()
|
||||
panic("panic before Goexit")
|
||||
}()
|
||||
runtime.Goexit()
|
||||
case "panic":
|
||||
defer func() {
|
||||
panic("panic after Goexit")
|
||||
}()
|
||||
runtime.Goexit()
|
||||
case "recovered-panic":
|
||||
defer func() {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
panic("recovered panic after Goexit")
|
||||
}()
|
||||
runtime.Goexit()
|
||||
case "recover-before-panic":
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
panic("bad recover")
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
panic("panic during Goexit")
|
||||
}()
|
||||
runtime.Goexit()
|
||||
case "recover-before-panic-loop":
|
||||
for i := 0; i < 2; i++ {
|
||||
defer func() {
|
||||
}()
|
||||
}
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
panic("bad recover")
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
panic("panic during Goexit")
|
||||
}()
|
||||
runtime.Goexit()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user