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 25, 2023
1 parent c05b59d commit 955a94f
Show file tree
Hide file tree
Showing 9 changed files with 717 additions and 0 deletions.
97 changes: 97 additions & 0 deletions co/co.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 func()

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

return r
}

type Routine struct {
done chan struct{}
resumed chan struct{}
status Status
}

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

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

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

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

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

func (r *Routine) Resume() {
if r.status == Dead {
return
}

r.resumed <- struct{}{}
<-r.done
}

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

func (r *Routine) 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

func (r Routines) ResumeAll() Routines {
for _, rout := range r {
rout.Resume()
}
return slices.DeleteFunc(r, func(r *Routine) 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

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

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

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

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) {
yield()
}
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.

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

import (
"math/rand"

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

var coroutines co.Routines

func main() {
pi.Update = func() {
if pi.MouseBtnp(pi.MouseLeft) {
//r := movePixel(pi.MousePos)
for j := 0; j < 8192; j++ {
coroutines = append(coroutines, co.Create(complexIterator()))
}
}
}

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

ebitengine.Run()
}

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

func moveHero(startX, stopX, minSpeed, maxSpeed int) func(yield co.Yield) {
anim := randomMove(startX, stopX, minSpeed, maxSpeed)

return func(yield co.Yield) {
for {
x, hasNext := anim()
pi.Set(x, 20, 7)
if hasNext {
yield()
} else {
return
}

}
}
}

// Reusable iterator which returns int. TODO THIS IS UGLY. I CANT USE co.Yield because it does not accept parameter
func randomMove(start, stop, minSpeed, maxSpeed int) func() (int, bool) {
pos := start

return func() (int, bool) {
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)
}

return pos, pos != stop
}
}

func complexIterator() func(yield co.Yield) {
return func(yield co.Yield) {
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) {
return func(yield co.Yield) {
for i := 0; i < iterations; i++ {
yield()
}
}
}
Loading

0 comments on commit 955a94f

Please sign in to comment.