Skip to content

Commit 70450f4

Browse files
committed
Add coroutines and iterators
1 parent c05b59d commit 70450f4

File tree

9 files changed

+715
-0
lines changed

9 files changed

+715
-0
lines changed

Diff for: co/co.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package co
2+
3+
import (
4+
"slices"
5+
)
6+
7+
const routineCancelled = "coroutine cancelled"
8+
9+
type Yield func()
10+
11+
func Create(f func(Yield)) *Routine {
12+
r := &Routine{ // 1 alloc
13+
resumed: make(chan struct{}), // 1 alloc
14+
done: make(chan struct{}), // 1 alloc
15+
status: Suspended,
16+
}
17+
go r.start(f) // 3 allocs
18+
19+
return r
20+
}
21+
22+
type Routine struct {
23+
done chan struct{}
24+
resumed chan struct{}
25+
status Status
26+
}
27+
28+
func (r *Routine) start(f func(Yield)) { // 1 alloc
29+
defer r.recoverAndDestroy()
30+
31+
_, ok := <-r.resumed // 2 allocs
32+
if !ok {
33+
panic(routineCancelled)
34+
}
35+
36+
r.status = Running
37+
f(r.yield)
38+
}
39+
40+
func (r *Routine) yield() {
41+
r.done <- struct{}{}
42+
r.status = Suspended
43+
if _, ok := <-r.resumed; !ok {
44+
panic(routineCancelled)
45+
}
46+
}
47+
48+
func (r *Routine) recoverAndDestroy() {
49+
p := recover()
50+
if p != nil && p != routineCancelled {
51+
panic("coroutine panicked")
52+
}
53+
r.status = Dead
54+
close(r.done)
55+
}
56+
57+
func (r *Routine) Resume() {
58+
if r.status == Dead {
59+
return
60+
}
61+
62+
r.resumed <- struct{}{}
63+
<-r.done
64+
}
65+
66+
func (r *Routine) Status() Status {
67+
return r.status
68+
}
69+
70+
func (r *Routine) Cancel() {
71+
if r.status == Dead {
72+
return
73+
}
74+
75+
close(r.resumed)
76+
<-r.done
77+
}
78+
79+
type Status string
80+
81+
const (
82+
// Normal Status = "normal" // This coroutine is currently waiting in coresume for another coroutine. (Either for the running coroutine, or for another normal coroutine)
83+
Running Status = "running" // This is the coroutine that's currently running - aka the one that just called costatus.
84+
Suspended Status = "suspended" // This coroutine is not running - either it has yielded or has never been resumed yet.
85+
Dead Status = "dead" // This coroutine has either returned or died due to an error.
86+
)
87+
88+
type Routines []*Routine
89+
90+
func (r Routines) ResumeAll() Routines {
91+
for _, rout := range r {
92+
rout.Resume()
93+
}
94+
return slices.DeleteFunc(r, func(r *Routine) bool {
95+
return r.Status() == Dead
96+
})
97+
}

Diff for: co/co_bench_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package co_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/elgopher/pi/co"
7+
)
8+
9+
func BenchmarkCreate(b *testing.B) {
10+
b.ReportAllocs()
11+
12+
var r *co.Routine
13+
14+
for i := 0; i < b.N; i++ {
15+
r = co.Create(f) // 6 allocs :( 12us :(
16+
}
17+
18+
_ = r
19+
}
20+
21+
func BenchmarkResume(b *testing.B) {
22+
b.ReportAllocs()
23+
24+
var r *co.Routine
25+
26+
for i := 0; i < b.N; i++ {
27+
r = co.Create(f) // 6 allocs
28+
r.Resume() // 1 alloc, 0.8us :(
29+
}
30+
_ = r
31+
}
32+
33+
func BenchmarkResumeUntilFinish(b *testing.B) {
34+
b.ReportAllocs()
35+
36+
var r *co.Routine
37+
38+
for i := 0; i < b.N; i++ {
39+
r = co.Create(f) // 6 allocs
40+
r.Resume() // 1 alloc, 0.8us :(
41+
r.Resume() // 1 alloc, 0.8us :(
42+
}
43+
_ = r
44+
}
45+
46+
func BenchmarkCancel(b *testing.B) {
47+
b.ReportAllocs()
48+
49+
var r *co.Routine
50+
51+
for i := 0; i < b.N; i++ {
52+
r = co.Create(f) // 6 allocs
53+
r.Cancel() // -2 alloc????
54+
}
55+
_ = r
56+
}
57+
58+
//go:noinline
59+
func f(yield co.Yield) {
60+
yield()
61+
}

Diff for: devtools/internal/lib/github_com-elgopher-pi.go

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: examples/coroutine/coroutine.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"math/rand"
5+
6+
"github.com/elgopher/pi"
7+
"github.com/elgopher/pi/co"
8+
"github.com/elgopher/pi/ebitengine"
9+
)
10+
11+
var coroutines co.Routines
12+
13+
func main() {
14+
pi.Update = func() {
15+
if pi.MouseBtnp(pi.MouseLeft) {
16+
//r := movePixel(pi.MousePos)
17+
for j := 0; j < 8192; j++ {
18+
coroutines = append(coroutines, co.Create(complexIterator()))
19+
}
20+
}
21+
}
22+
23+
pi.Draw = func() {
24+
pi.Cls()
25+
coroutines = coroutines.ResumeAll()
26+
//devtools.Export("coroutines", coroutines)
27+
}
28+
29+
ebitengine.Run()
30+
}
31+
32+
func movePixel(pos pi.Position) func(yield co.Yield) {
33+
return func(yield co.Yield) {
34+
for i := 0; i < 64; i++ {
35+
pi.Set(pos.X+i, pos.Y+i, byte(rand.Intn(16)))
36+
yield()
37+
yield()
38+
}
39+
}
40+
}
41+
42+
func moveHero(startX, stopX, minSpeed, maxSpeed int) func(yield co.Yield) {
43+
anim := randomMove(startX, stopX, minSpeed, maxSpeed)
44+
45+
return func(yield co.Yield) {
46+
for {
47+
x, hasNext := anim()
48+
pi.Set(x, 20, 7)
49+
if hasNext {
50+
yield()
51+
} else {
52+
return
53+
}
54+
55+
}
56+
}
57+
}
58+
59+
// Reusable iterator which returns int. TODO THIS IS UGLY. I CANT USE co.Yield because it does not accept parameter
60+
func randomMove(start, stop, minSpeed, maxSpeed int) func() (int, bool) {
61+
pos := start
62+
63+
return func() (int, bool) {
64+
speed := rand.Intn(maxSpeed - minSpeed)
65+
if stop > start {
66+
pos = pi.MinInt(stop, pos+speed) // move pos in stop direction by random speed
67+
} else {
68+
pos = pi.MaxInt(stop, pos-speed)
69+
}
70+
71+
return pos, pos != stop
72+
}
73+
}
74+
75+
func complexIterator() func(yield co.Yield) {
76+
return func(yield co.Yield) {
77+
sleep(10)(yield)
78+
moveHero(10, 120, 5, 10)(yield)
79+
sleep(20)(yield)
80+
moveHero(120, 10, 2, 10)(yield)
81+
}
82+
}
83+
84+
func sleep(iterations int) func(yield co.Yield) {
85+
return func(yield co.Yield) {
86+
for i := 0; i < iterations; i++ {
87+
yield()
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)