Skip to content

Commit efc72a0

Browse files
committed
Add coroutines and iterators
1 parent c05b59d commit efc72a0

File tree

9 files changed

+736
-0
lines changed

9 files changed

+736
-0
lines changed

co/co.go

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

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[struct{}]
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[struct{}]
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[struct{}]
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[struct{}]
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[struct{}]) {
60+
yield(struct{}{})
61+
}

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.

examples/coroutine/coroutine.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"math/rand"
5+
"net/http"
6+
7+
"github.com/elgopher/pi"
8+
"github.com/elgopher/pi/co"
9+
"github.com/elgopher/pi/ebitengine"
10+
)
11+
12+
var coroutines co.Routines
13+
14+
func main() {
15+
go func() {
16+
http.ListenAndServe("localhost:6060", nil)
17+
}()
18+
19+
pi.Update = func() {
20+
if pi.MouseBtnp(pi.MouseLeft) {
21+
//r := movePixel(pi.MousePos)
22+
for j := 0; j < 8000; j++ { // (~9KB per COROUTINE). Pico-8 has 4000 coroutines limit
23+
coroutines = append(coroutines, co.Create(complexCoroutine())) // complexCoroutine is 2 coroutines - 18KB in total
24+
}
25+
}
26+
}
27+
28+
pi.Draw = func() {
29+
pi.Cls()
30+
coroutines = coroutines.ResumeAll()
31+
//devtools.Export("coroutines", coroutines)
32+
}
33+
34+
ebitengine.Run()
35+
}
36+
37+
func movePixel(pos pi.Position) func(yield co.Yield[struct{}]) {
38+
return func(yield co.Yield[struct{}]) {
39+
for i := 0; i < 64; i++ {
40+
pi.Set(pos.X+i, pos.Y+i, byte(rand.Intn(16)))
41+
yield(struct{}{})
42+
yield(struct{}{})
43+
}
44+
}
45+
}
46+
47+
func moveHero(startX, stopX, minSpeed, maxSpeed int) func(yield co.Yield[struct{}]) {
48+
anim := co.Create(randomMove(startX, stopX, minSpeed, maxSpeed))
49+
50+
return func(yield co.Yield[struct{}]) {
51+
for {
52+
x, hasMore := anim.Resume()
53+
pi.Set(x, 20, 7)
54+
if hasMore {
55+
yield(struct{}{})
56+
} else {
57+
return
58+
}
59+
60+
}
61+
}
62+
}
63+
64+
// Reusable coroutine which returns int.
65+
func randomMove(start, stop, minSpeed, maxSpeed int) func(yield co.Yield[int]) {
66+
pos := start
67+
68+
return func(yield co.Yield[int]) {
69+
for {
70+
speed := rand.Intn(maxSpeed - minSpeed)
71+
if stop > start {
72+
pos = pi.MinInt(stop, pos+speed) // move pos in stop direction by random speed
73+
} else {
74+
pos = pi.MaxInt(stop, pos-speed)
75+
}
76+
77+
if pos == stop {
78+
return
79+
} else {
80+
yield(pos)
81+
}
82+
}
83+
}
84+
}
85+
86+
func complexCoroutine() func(yield co.Yield[struct{}]) {
87+
return func(yield co.Yield[struct{}]) {
88+
sleep(10)(yield)
89+
moveHero(10, 120, 5, 10)(yield)
90+
sleep(20)(yield)
91+
moveHero(120, 10, 2, 10)(yield)
92+
}
93+
}
94+
95+
func sleep(iterations int) func(yield co.Yield[struct{}]) {
96+
return func(yield co.Yield[struct{}]) {
97+
for i := 0; i < iterations; i++ {
98+
yield(struct{}{})
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)