Skip to content

Commit ae8a598

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

File tree

9 files changed

+753
-0
lines changed

9 files changed

+753
-0
lines changed

coro/coro.go

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

coro/coro_bench_test.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package coro_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/elgopher/pi/coro"
7+
)
8+
9+
func BenchmarkNew(b *testing.B) {
10+
b.ReportAllocs()
11+
12+
var r *coro.Routine[struct{}]
13+
14+
for i := 0; i < b.N; i++ {
15+
r = coro.New(f2) // 7 allocs :( 4us on windows :( But on linux it is 1us and 5 allocs!
16+
}
17+
18+
_ = r
19+
}
20+
21+
func BenchmarkCreate(b *testing.B) {
22+
b.ReportAllocs()
23+
24+
var r *coro.Routine[struct{}]
25+
26+
for i := 0; i < b.N; i++ {
27+
r = coro.WithReturn(f) // 6 allocs :( 4us on windows :( But on linux it is 1us and 5 allocs!
28+
}
29+
30+
_ = r
31+
}
32+
33+
func BenchmarkResume(b *testing.B) {
34+
b.ReportAllocs()
35+
36+
var r *coro.Routine[struct{}]
37+
38+
for i := 0; i < b.N; i++ {
39+
r = coro.WithReturn(f) // 6 allocs
40+
r.Resume() // 1 alloc, 0.8us :(
41+
}
42+
_ = r
43+
}
44+
45+
func BenchmarkResumeUntilFinish(b *testing.B) {
46+
b.ReportAllocs()
47+
48+
var r *coro.Routine[struct{}]
49+
50+
for i := 0; i < b.N; i++ {
51+
r = coro.WithReturn(f) // 6 allocs
52+
r.Resume() // 1 alloc, 0.8us :(
53+
r.Resume() // 1 alloc, 0.8us :(
54+
}
55+
_ = r
56+
}
57+
58+
func BenchmarkCancel(b *testing.B) {
59+
b.ReportAllocs()
60+
61+
var r *coro.Routine[struct{}]
62+
63+
for i := 0; i < b.N; i++ {
64+
r = coro.WithReturn(f) // 6 allocs
65+
r.Cancel() // -2 alloc????
66+
}
67+
_ = r
68+
}
69+
70+
//go:noinline
71+
func f2(yield coro.Yield) {
72+
yield()
73+
}
74+
75+
//go:noinline
76+
func f(yield coro.YieldReturn[struct{}]) {
77+
yield(struct{}{})
78+
}

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

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

0 commit comments

Comments
 (0)