package coroutine
import (
"time"
"sync"
"log"
)
// The base struct that has all the functions necessary to operate on a coroutine. It is designed to be used in one of
// two ways:
// - As the argument to a func passed to one of the Start functions. It can then be used as a proxy for the
// coroutine function. e.g.: `msg := e.Recv()`
// - Embedded directly into another struct, so that the struct can call the coroutine functions as if they were its
// own. e.g.: `type A struct { coroutine.Embeddable }` The struct that embeds the coroutine struct can then
// implement the Start method on the Starter interface and an instance of it can be passed directly to one of the
// Start functions. Embeddable MUST be embedded as a non-pointer, and the struct embedding it MUST be used as a
// pointer.
type
Embeddable
struct {
id
uint64
name
string
waitTimer *time.Timer
receiver
chan
bool
receiveTimer *time.Timer
mailbox []
interface{}
mailboxLock sync.Mutex
running
bool
}
// Pauses execution of this coroutine for the given duration to allow other coroutines to run.
//
// If this coroutine has been stopped by external code using the Ref returned by all Start functions, then it will
// immediately stop, and no further code outside of deferred functions will be executed in this coroutine.
func (e *Embeddable)
Pause(d time.Duration) {
if !e.running {
// Every coroutine is wrapped in a function that recovers from a panic, so this is guaranteed to immediately
// stop execution of the coroutine completely without stopping the rest of the program.
panic(Stop{})
}
resetTimer(e.waitTimer, d)
<-e.waitTimer.C
// Since there's a period of time that this is doing nothing, there's a chance that external code could stop
// this coroutine while it's paused. So we check that before returning control to the coroutine.
if !e.running {
panic(Stop{})
}
}
// Instead of constructing a new Timer object, which can be expensive if done frequently, we reset an existing one
// to a given duration immediately before using it.
func
resetTimer(t *time.Timer, d time.Duration) {
if !t.
Stop() {
// I've seen cases where Stop returns false, but there isn't an item in the channel, causing the receive to
// hang forever.
select {
case <-t.C:
default:
}
}
t.
Reset(d)
}
// Checks the mailbox for any sent messages. If none are in the mailbox, this function will halt the coroutine until
// one gets sent.
//
// If this coroutine has been stopped by external code using the Ref returned by all Start functions, then it will
// immediately stop, and no further code outside of deferred functions will be executed in this coroutine.
func (e *Embeddable)
Recv()
interface{} {
if !e.running {
panic(Stop{})
}
e.mailboxLock.
Lock()
if
len(e.mailbox) ==
0 {
e.mailboxLock.
Unlock()
<-e.receiver
if !e.running {
panic(Stop{})
}
e.mailboxLock.
Lock()
}
r := e.mailbox[
0]
e.mailbox = e.mailbox[
1:]
e.mailboxLock.
Unlock()
return r
}
// Checks if the mailbox contains anything. If it does, that value and true are returned. If it doesn't, the
// coroutine will pause for up to duration time. If a value is put into the mailbox within that time, that value and
// true are returned. If nothing was put into the mailbox during that time, nil and false are returned.
//
// If this coroutine has been stopped by external code using the Ref returned by all Start functions, then it will
// immediately stop, and no further code outside of deferred functions will be executed in this coroutine.
func (e *Embeddable)
RecvFor(d time.Duration) (
interface{},
bool) {
if !e.running {
panic(Stop{})
}
if d <=
0 {
return e.
RecvImmediate()
}
e.mailboxLock.
Lock()
if
len(e.mailbox) ==
0 {
e.mailboxLock.
Unlock()
resetTimer(e.receiveTimer, d)
select {
case <-e.receiver:
case <-e.receiveTimer.C:
}
if !e.running {
panic(Stop{})
}
e.mailboxLock.
Lock()
}
if
len(e.mailbox) ==
0 {
e.mailboxLock.
Unlock()
return
nil,
false
}
r := e.mailbox[
0]
e.mailbox = e.mailbox[
1:]
e.mailboxLock.
Unlock()
return r,
true
}
// Checks if the mailbox contains anything. If it doesn't, nil and false are returned. If something is in the mailbox,
// that value and true are returned. The found value is removed from the mailbox.
//
// If this coroutine has been stopped by external code using the Ref returned by all Start functions, then it will
// immediately stop, and no further code outside of deferred functions will be executed in this coroutine.
func (e *Embeddable)
RecvImmediate() (
interface{},
bool) {
if !e.running {
panic(Stop{})
}
e.mailboxLock.
Lock()
defer e.mailboxLock.
Unlock()
if
len(e.mailbox) ==
0 {
return
nil,
false
}
r := e.mailbox[
0]
e.mailbox = e.mailbox[
1:]
return r,
true
}
// Immediately stop this coroutine. No more code in the coroutine will run, so be sure to do any cleanup work before
// calling this function, or have a deferred function that will do your cleanup work.
func (e *Embeddable)
Stop() {
if !e.running {
log.
Printf(
"Coroutine [%v / %s] attempted to stop itself when it isn't running, possible bug found.",
e.id, e.name)
}
e.running =
false
panic(Stop{})
}
package coroutine
import (
"log"
)
// Simple reference to a coroutine. Allows external code to send messages to that coroutine, stop it, and check
// various bits of data about it.
type
Ref
interface {
Send(v
interface{})
Running()
bool
Name()
string
Id()
uint64
Stop()
}
// Separate struct from the Embeddable coroutine so that the Stop function can behave differently for external code
// versus internal to the coroutine.
type
embeddableRef
struct {
e *Embeddable
}
// Puts a message into the mailbox of the coroutine this references.
func (r *embeddableRef)
Send(v
interface{}) {
r.e.mailboxLock.
Lock()
r.e.mailbox =
append(r.e.mailbox, v)
r.e.mailboxLock.
Unlock()
// If the coroutine is waiting on the mailbox, let it know. Otherwise continue immediately so the sender
// doesn't get blocked.
select {
case r.e.receiver <-
true:
default:
}
}
// Whether or not the coroutine this references is still running.
func (r *embeddableRef)
Running()
bool {
return r.e.running
}
// The name given to the coroutine this references at start time. If no name was given, a generic name is assigned.
func (r *embeddableRef)
Name()
string {
return r.e.name
}
// The unique ID of the coroutine this references.
func (r *embeddableRef)
Id()
uint64 {
return r.e.id
}
// Stops the coroutine this references. Will not immediately halt execution of the coroutine, but when it calls any
// of the methods on the Embeddable struct, execution will halt at that point. So if it's in a tight loop, that
// loop will finish.
func (r *embeddableRef)
Stop() {
if !r.e.running {
log.
Printf(
"Coroutine [%v / %s] attempted to be stopped when it isn't running, possible bug found.",
r.e.id, r.e.name)
return
}
r.e.running =
false
// If the coroutine is in the middle of attempting to receive something, immediately cause it to stop attempting
// to receive so it can detect that it needs to stop.
select {
case r.e.receiver <-
true:
default:
}
}
package coroutine
import (
"sync"
"time"
)
// The type that is passed to panic whenever a coroutine is stopping itself. If a coroutine function has a
// deferred function which calls recover, this should be checked for to be ignored. If the deferred functions which
// recovers would cause a panic, and this was the value that came from the return value of recover, this should be
// what is given to panic to continue normal execution of the program.
type
Stop
struct {
}
func (s *Stop)
Error()
string {
return
"coroutine stop"
}
// Signature of the func that can be started as a coroutine. Receives the embeddable struct so that the func can
// call methods on it to act as a coroutine.
type
Function
func(embeddable *Embeddable)
type
Starter
interface {
Start()
Embedded() *Embeddable
}
const (
defaultName =
"Default Coroutine Name"
)
var (
nextId
uint64 =
1
nextIdLock sync.Mutex
)
func
StartFunc(f Function) Ref {
return
StartFuncName(defaultName, f)
}
func
StartFuncName(name
string, f Function) Ref {
next := &Embeddable{
name: name,
waitTimer: time.
NewTimer(
0),
receiver:
make(
chan
bool),
receiveTimer: time.
NewTimer(
0),
running:
true,
}
nextIdLock.
Lock()
next.id = nextId
nextId++
nextIdLock.
Unlock()
go
func() {
defer
func() {
// Ensure external code will know that this coroutine is stopped if the program doesn't end due to the
// panic.
next.running =
false
// Close down all the coroutine's resources.
next.waitTimer.
Stop()
next.receiveTimer.
Stop()
close(next.receiver)
if
e :=
recover(); e !=
nil {
if
_,
ok := e.(Stop); ok {
// Stop requested for this coroutine, so we just let the goroutine end.
}
else {
// Repanic since it came from code that isn't part of the coroutine library.
panic(e)
}
}
}()
f(next)
}()
return &embeddableRef{next}
}
func
Start(s Starter) Ref {
return
StartName(defaultName, s)
}
func (e *Embeddable)
Embedded() *Embeddable {
return e
}
func
StartName(name
string, s Starter) Ref {
e := s.
Embedded()
e.name = name
e.waitTimer = time.
NewTimer(
0)
e.receiver =
make(
chan
bool)
e.receiveTimer = time.
NewTimer(
0)
e.running =
true
nextIdLock.
Lock()
e.id = nextId
nextId++
nextIdLock.
Unlock()
go
func() {
defer
func() {
// Ensure external code will know that this coroutine is stopped if the program doesn't end due to the
// panic.
e.running =
false
// Close down all the coroutine's resources.
e.waitTimer.
Stop()
e.receiveTimer.
Stop()
close(e.receiver)
if
e :=
recover(); e !=
nil {
if
_,
ok := e.(Stop); ok {
// Stop requested for this coroutine, so we just let the goroutine end.
}
else {
// Repanic since it came from code that isn't part of the coroutine library.
panic(e)
}
}
}()
s.
Start()
}()
return &embeddableRef{e}
}
Example
type MyCoroutine struct {
line int
coroutine.Embeddable
}
func (c *MyCoroutine) Start() {
for {
switch msg := c.Recv().(type) {
case int:
fmt.Printf("Message %v: Received integer [%v]\n", c.line, msg)
case string:
fmt.Printf("Message %v: Received string [%s]\n", c.line, msg)
}
c.line++
}
fmt.Println("This never prints")
}
func main() {
r := coroutine.Start(&MyCoroutine{})
for i := 0; i < 100; i++ {
r.Send(i);
r.Send("msg " + strconv.Itoa(i))
}
time.Sleep(5 * time.Millisecond)
r.Stop()
time.Sleep(5 * time.Millisecond)
fmt.Println("All done. All messages should have been printed, but not the println at the end of Start.")
}