Skip to content

Commit ae74ec1

Browse files
committed
all: support capturing panics
1 parent 7eebd51 commit ae74ec1

File tree

3 files changed

+158
-21
lines changed

3 files changed

+158
-21
lines changed

README.md

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,70 @@ import "golang.design/x/mainthread"
2424

2525
func main() { mainthread.Init(fn) }
2626

27-
// fn is the actual main function
27+
// fn is the actual main function
2828
func fn() {
29-
// ... do whatever you want to do ...
29+
// ... do stuff ...
3030

3131
// mainthread.Call returns when f1 returns. Note that if f1 blocks
3232
// it will also block the execution of any subsequent calls on the
3333
// main thread.
3434
mainthread.Call(f1)
3535

36-
// ... do whatever you want to do ...
36+
// ... do stuff ...
37+
3738

3839
// mainthread.Go returns immediately and f2 is scheduled to be
3940
// executed in the future.
4041
mainthread.Go(f2)
4142

42-
// ... do whatever you want to do ...
43+
// ... do stuff ...
4344
}
4445

4546
func f1() { ... }
4647
func f2() { ... }
4748
```
4849

50+
If the given function triggers a panic, and called via `mainthread.Call`,
51+
then the panic will be propagated to the same goroutine. One can capture
52+
that panic, when possible:
53+
54+
```go
55+
defer func() {
56+
if r := recover(); r != nil {
57+
println(r)
58+
}
59+
}()
60+
61+
mainthread.Call(func() { ... }) // if panic
62+
```
63+
64+
If the given function triggers a panic, and called via `mainthread.Go`,
65+
then the panic will be cached internally, until a call to the `Error()` method:
66+
67+
```go
68+
mainthread.Go(func() { ... }) // if panics
69+
70+
// ... do stuff ...
71+
72+
if err := mainthread.Error(); err != nil { // can be captured here.
73+
println(err)
74+
}
75+
```
76+
77+
Note that a panic happens before `mainthread.Error()` returning the
78+
panicked error. If one needs to guarantee `mainthread.Error()` indeed
79+
captured the panic, a dummy function can be used as synchornization:
80+
81+
```go
82+
mainthread.Go(func() { panic("die") }) // if panics
83+
mainthread.Call(func() {}) // for execution synchronization
84+
err := mainthread.Error() // err must be non-nil
85+
```
86+
87+
88+
It is possible to cache up to a maximum of 42 panicked errors.
89+
More errors are ignored.
90+
4991
## When do you need this package?
5092

5193
Read this to learn more about the design purpose of this package:

mainthread.go

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,63 @@
1616
//
1717
// // fn is the actual main function
1818
// func fn() {
19-
// // ... do whatever you want to do ...
19+
// // ... do stuff ...
2020
//
21-
// // mainthread.Call returns when f1 returns. Note that if f1
22-
// // blocks it will also block the execution of any subsequent
23-
// // calls on the main thread.
21+
// // mainthread.Call returns when f1 returns. Note that if f1 blocks
22+
// // it will also block the execution of any subsequent calls on the
23+
// // main thread.
2424
// mainthread.Call(f1)
2525
//
26-
// // ... do whatever you want to do ...
26+
// // ... do stuff ...
27+
//
2728
//
2829
// // mainthread.Go returns immediately and f2 is scheduled to be
2930
// // executed in the future.
3031
// mainthread.Go(f2)
3132
//
32-
// // ... do whatever you want to do ...
33+
// // ... do stuff ...
3334
// }
3435
//
3536
// func f1() { ... }
3637
// func f2() { ... }
38+
//
39+
// If the given function triggers a panic, and called via `mainthread.Call`,
40+
// then the panic will be propagated to the same goroutine. One can capture
41+
// that panic, when possible:
42+
//
43+
// defer func() {
44+
// if r := recover(); r != nil {
45+
// println(r)
46+
// }
47+
// }()
48+
//
49+
// mainthread.Call(func() { ... }) // if panic
50+
//
51+
// If the given function triggers a panic, and called via `mainthread.Go`,
52+
// then the panic will be cached internally, until a call to the `Error()` method:
53+
//
54+
// mainthread.Go(func() { ... }) // if panics
55+
//
56+
// // ... do stuff ...
57+
//
58+
// if err := mainthread.Error(); err != nil { // can be captured here.
59+
// println(err)
60+
// }
61+
//
62+
// Note that a panic happens before `mainthread.Error()` returning the
63+
// panicked error. If one needs to guarantee `mainthread.Error()` indeed
64+
// captured the panic, a dummy function can be used as synchornization:
65+
//
66+
// mainthread.Go(func() { panic("die") }) // if panics
67+
// mainthread.Call(func() {}) // for execution synchronization
68+
// err := mainthread.Error() // err must be non-nil
69+
//
70+
// It is possible to cache up to a maximum of 42 panicked errors.
71+
// More errors are ignored.
3772
package mainthread // import "golang.design/x/mainthread"
3873

3974
import (
75+
"fmt"
4076
"runtime"
4177
"sync"
4278
)
@@ -50,23 +86,39 @@ func init() {
5086
//
5187
// Init must be called in the main.main function.
5288
func Init(main func()) {
53-
done := donePool.Get().(chan struct{})
89+
done := donePool.Get().(chan error)
5490
defer donePool.Put(done)
5591

5692
go func() {
5793
defer func() {
58-
done <- struct{}{}
94+
done <- nil
5995
}()
6096
main()
6197
}()
6298

6399
for {
64100
select {
65101
case f := <-funcQ:
66-
f.fn()
67-
if f.done != nil {
68-
f.done <- struct{}{}
69-
}
102+
func() {
103+
defer func() {
104+
r := recover()
105+
if f.done != nil {
106+
if r != nil {
107+
f.done <- fmt.Errorf("%v", r)
108+
} else {
109+
f.done <- nil
110+
}
111+
} else {
112+
if r != nil {
113+
select {
114+
case erroQ <- fmt.Errorf("%v", r):
115+
default:
116+
}
117+
}
118+
}
119+
}()
120+
f.fn()
121+
}()
70122
case <-done:
71123
return
72124
}
@@ -75,26 +127,44 @@ func Init(main func()) {
75127

76128
// Call calls f on the main thread and blocks until f finishes.
77129
func Call(f func()) {
78-
done := donePool.Get().(chan struct{})
130+
done := donePool.Get().(chan error)
79131
defer donePool.Put(done)
80132

81-
funcQ <- funcData{fn: f, done: done}
82-
<-done
133+
data := funcData{fn: f, done: done}
134+
funcQ <- data
135+
if err := <-done; err != nil {
136+
panic(err)
137+
}
83138
}
84139

85140
// Go schedules f to be called on the main thread.
86141
func Go(f func()) {
87142
funcQ <- funcData{fn: f}
88143
}
89144

145+
// Error returns an error that is captured if there are any panics
146+
// happened on the mainthread.
147+
//
148+
// It is possible to cache up to a maximum of 42 panicked errors.
149+
// More errors are ignored.
150+
func Error() error {
151+
select {
152+
case err := <-erroQ:
153+
return err
154+
default:
155+
return nil
156+
}
157+
}
158+
90159
var (
91160
funcQ = make(chan funcData, runtime.GOMAXPROCS(0))
161+
erroQ = make(chan error, 42)
92162
donePool = sync.Pool{New: func() interface{} {
93-
return make(chan struct{})
163+
return make(chan error)
94164
}}
95165
)
96166

97167
type funcData struct {
98168
fn func()
99-
done chan struct{}
169+
done chan error
100170
}

mainthread_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,28 @@ func TestGo(t *testing.T) {
9292
case <-done:
9393
}
9494
}
95+
96+
func TestPanickedFuncCall(t *testing.T) {
97+
defer func() {
98+
if r := recover(); r != nil {
99+
return
100+
}
101+
t.Fatalf("expected to panic, but actually not")
102+
}()
103+
104+
mainthread.Call(func() {
105+
panic("die")
106+
})
107+
}
108+
109+
func TestPanickedFuncGo(t *testing.T) {
110+
defer func() {
111+
if err := mainthread.Error(); err != nil {
112+
return
113+
}
114+
t.Fatalf("expected to panic, but actually not")
115+
}()
116+
117+
mainthread.Go(func() { panic("die") })
118+
mainthread.Call(func() {}) // for sync
119+
}

0 commit comments

Comments
 (0)