Skip to content

Commit

Permalink
Add coroutines and iterators
Browse files Browse the repository at this point in the history
  • Loading branch information
elgopher committed Aug 26, 2023
1 parent c05b59d commit efc72a0
Show file tree
Hide file tree
Showing 9 changed files with 736 additions and 0 deletions.
98 changes: 98 additions & 0 deletions co/co.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package co

import (
"slices"

Check failure on line 4 in co/co.go

View workflow job for this annotation

GitHub Actions / all (1.20)

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.20.7/x64/src/slices)
)

const routineCancelled = "coroutine cancelled"

type Yield[V any] func(V)

func Create[V any](f func(Yield[V])) *Routine[V] {
r := &Routine[V]{ // 1 alloc
resumed: make(chan struct{}), // 1 alloc
done: make(chan V), // 1 alloc
status: Suspended,
}
go r.start(f) // 3 allocs

return r
}

type Routine[V any] struct {
done chan V
resumed chan struct{}
status Status
}

func (r *Routine[V]) start(f func(Yield[V])) { // 1 alloc
defer r.recoverAndDestroy()

_, ok := <-r.resumed // 2 allocs
if !ok {
panic(routineCancelled)
}

r.status = Running
f(r.yield)
}

func (r *Routine[V]) yield(v V) {
r.done <- v
r.status = Suspended
if _, ok := <-r.resumed; !ok {
panic(routineCancelled)
}
}

func (r *Routine[V]) recoverAndDestroy() {
p := recover()
if p != nil && p != routineCancelled {
panic("coroutine panicked")
}
r.status = Dead
close(r.done)
}

func (r *Routine[V]) Resume() (value V, hasMore bool) {
if r.status == Dead {
return
}

r.resumed <- struct{}{}
value, hasMore = <-r.done
return
}

func (r *Routine[V]) Status() Status {
return r.status
}

func (r *Routine[V]) Cancel() {
if r.status == Dead {
return
}

close(r.resumed)
<-r.done
}

type Status string

const (
// Normal Status = "normal" // This coroutine is currently waiting in coresume for another coroutine. (Either for the running coroutine, or for another normal coroutine)
Running Status = "running" // This is the coroutine that's currently running - aka the one that just called costatus.
Suspended Status = "suspended" // This coroutine is not running - either it has yielded or has never been resumed yet.
Dead Status = "dead" // This coroutine has either returned or died due to an error.
)

type Routines []*Routine[struct{}]

func (r Routines) ResumeAll() Routines {
for _, rout := range r {
rout.Resume()
}
return slices.DeleteFunc(r, func(r *Routine[struct{}]) bool {
return r.Status() == Dead
})
}
61 changes: 61 additions & 0 deletions co/co_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package co_test

import (
"testing"

"github.com/elgopher/pi/co"
)

func BenchmarkCreate(b *testing.B) {
b.ReportAllocs()

var r *co.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = co.Create(f) // 6 allocs :( 12us :(
}

_ = r
}

func BenchmarkResume(b *testing.B) {
b.ReportAllocs()

var r *co.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = co.Create(f) // 6 allocs
r.Resume() // 1 alloc, 0.8us :(
}
_ = r
}

func BenchmarkResumeUntilFinish(b *testing.B) {
b.ReportAllocs()

var r *co.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = co.Create(f) // 6 allocs
r.Resume() // 1 alloc, 0.8us :(
r.Resume() // 1 alloc, 0.8us :(
}
_ = r
}

func BenchmarkCancel(b *testing.B) {
b.ReportAllocs()

var r *co.Routine[struct{}]

for i := 0; i < b.N; i++ {
r = co.Create(f) // 6 allocs
r.Cancel() // -2 alloc????
}
_ = r
}

//go:noinline
func f(yield co.Yield[struct{}]) {
yield(struct{}{})
}
2 changes: 2 additions & 0 deletions devtools/internal/lib/github_com-elgopher-pi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 101 additions & 0 deletions examples/coroutine/coroutine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"math/rand"
"net/http"

"github.com/elgopher/pi"
"github.com/elgopher/pi/co"
"github.com/elgopher/pi/ebitengine"
)

var coroutines co.Routines

func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()

pi.Update = func() {
if pi.MouseBtnp(pi.MouseLeft) {
//r := movePixel(pi.MousePos)
for j := 0; j < 8000; j++ { // (~9KB per COROUTINE). Pico-8 has 4000 coroutines limit
coroutines = append(coroutines, co.Create(complexCoroutine())) // complexCoroutine is 2 coroutines - 18KB in total
}
}
}

pi.Draw = func() {
pi.Cls()
coroutines = coroutines.ResumeAll()
//devtools.Export("coroutines", coroutines)
}

ebitengine.Run()
}

func movePixel(pos pi.Position) func(yield co.Yield[struct{}]) {
return func(yield co.Yield[struct{}]) {
for i := 0; i < 64; i++ {
pi.Set(pos.X+i, pos.Y+i, byte(rand.Intn(16)))
yield(struct{}{})
yield(struct{}{})
}
}
}

func moveHero(startX, stopX, minSpeed, maxSpeed int) func(yield co.Yield[struct{}]) {
anim := co.Create(randomMove(startX, stopX, minSpeed, maxSpeed))

return func(yield co.Yield[struct{}]) {
for {
x, hasMore := anim.Resume()
pi.Set(x, 20, 7)
if hasMore {
yield(struct{}{})
} else {
return
}

}
}
}

// Reusable coroutine which returns int.
func randomMove(start, stop, minSpeed, maxSpeed int) func(yield co.Yield[int]) {
pos := start

return func(yield co.Yield[int]) {
for {
speed := rand.Intn(maxSpeed - minSpeed)
if stop > start {
pos = pi.MinInt(stop, pos+speed) // move pos in stop direction by random speed
} else {
pos = pi.MaxInt(stop, pos-speed)
}

if pos == stop {
return
} else {
yield(pos)
}
}
}
}

func complexCoroutine() func(yield co.Yield[struct{}]) {
return func(yield co.Yield[struct{}]) {
sleep(10)(yield)
moveHero(10, 120, 5, 10)(yield)
sleep(20)(yield)
moveHero(120, 10, 2, 10)(yield)
}
}

func sleep(iterations int) func(yield co.Yield[struct{}]) {
return func(yield co.Yield[struct{}]) {
for i := 0; i < iterations; i++ {
yield(struct{}{})
}
}
}
Loading

0 comments on commit efc72a0

Please sign in to comment.