diff --git a/builder/sizes_test.go b/builder/sizes_test.go index 5e809760f..5f75d484b 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -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 diff --git a/main_test.go b/main_test.go index 53bade009..b7dfa6b0f 100644 --- a/main_test.go +++ b/main_test.go @@ -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()) + } + }) } } diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go index 546b6841b..0f7437067 100644 --- a/src/internal/task/task_asyncify.go +++ b/src/internal/task/task_asyncify.go @@ -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) } diff --git a/src/internal/task/task_exit.go b/src/internal/task/task_exit.go new file mode 100644 index 000000000..0c2bb8ee8 --- /dev/null +++ b/src/internal/task/task_exit.go @@ -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") +} diff --git a/src/internal/task/task_stack.go b/src/internal/task/task_stack.go index b6c4a5df9..9db5c5890 100644 --- a/src/internal/task/task_stack.go +++ b/src/internal/task/task_stack.go @@ -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) } diff --git a/src/internal/task/task_threads.c b/src/internal/task/task_threads.c index 5ff519d91..5e94379b9 100644 --- a/src/internal/task/task_threads.c +++ b/src/internal/task/task_threads.c @@ -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); diff --git a/src/internal/task/task_threads.go b/src/internal/task/task_threads.go index 2085fa979..0d14eae34 100644 --- a/src/internal/task/task_threads.go +++ b/src/internal/task/task_threads.go @@ -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 diff --git a/src/runtime/panic.go b/src/runtime/panic.go index cec4f74fe..0afd90a11 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -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) diff --git a/src/runtime/runtime_windows.go b/src/runtime/runtime_windows.go index 839356510..f961a5670 100644 --- a/src/runtime/runtime_windows.go +++ b/src/runtime/runtime_windows.go @@ -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 diff --git a/src/runtime/scheduler_cooperative.go b/src/runtime/scheduler_cooperative.go index d30ec4196..5970dae38 100644 --- a/src/runtime/scheduler_cooperative.go +++ b/src/runtime/scheduler_cooperative.go @@ -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) diff --git a/src/runtime/scheduler_cores.go b/src/runtime/scheduler_cores.go index 25a2ada87..cfec67500 100644 --- a/src/runtime/scheduler_cores.go +++ b/src/runtime/scheduler_cores.go @@ -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) { diff --git a/src/runtime/scheduler_none.go b/src/runtime/scheduler_none.go index ed259b0d1..a9fc65aa8 100644 --- a/src/runtime/scheduler_none.go +++ b/src/runtime/scheduler_none.go @@ -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. } diff --git a/src/runtime/scheduler_threads.go b/src/runtime/scheduler_threads.go index acc1281f2..5b6638b98 100644 --- a/src/runtime/scheduler_threads.go +++ b/src/runtime/scheduler_threads.go @@ -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() } diff --git a/testdata/goexit.go b/testdata/goexit.go index 15c828e9d..fd6c0d2cd 100644 --- a/testdata/goexit.go +++ b/testdata/goexit.go @@ -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() + } }