Question: Question:
I want to reproduce features like Lua's coroutines and Ruby's Fiber in Go, but I can't think of a good way.
Using Goroutine as follows, similar things are possible but problematic.
package main
import "fmt"
func main(){
ch := make (chan int)
go func() {
i := 0
for {
ch <- i
i++
}
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
In Lua and Ruby, coroutines are subject to GC even if they can be resumed. However, in Go, the blocked Goroutine is not the target of GC, so a memory leak will occur.
In the example below, ch
is only referenced in Goroutine, so unblocking will not occur forever, but Goroutine remains.
package main
import (
"runtime"
"fmt"
)
func main(){
for {
go func(ch <-chan int) {
<-ch
}(make(chan int))
runtime.GC()
fmt.Println(runtime.NumGoroutine())
}
}
Is there any good solution?
Answer: Answer:
The "coroutine" in the question is a concept that coroutines generally represent.
- A mechanism that interrupts processing at the point when the user (programmer) explicitly calls
yield
and transfers control to other processing - On the contrary, the process is not switched unless the user explicitly indicates it, a non-preemptive concurrency mechanism.
If it refers to, goroutine is not a "coroutine" because the user cannot explicitly specify when to switch processes or what process should be executed next, and currently to realize coroutines. Since it is necessary to have a mechanism to save the execution state of the processing (stack, code execution position, etc.) somewhere, if such a mechanism is not prepared in the processing system, it cannot be implemented no matter how much the user devises the library. I think the answer is that "coroutines" cannot be realized with Go.
If the coroutine in question is intended to be a way to easily implement an iterator by interrupting the execution of a function (eg Python yield
) and interrupting the iterator does not cause a memory leak. If so, as you pointed out, the goroutine is not subject to GC, so I think there is no choice but to send a stop signal to the goroutine used internally by the iterator and explicitly stop the goroutine.
In the following example, a channel quit
for end notification is created, and branching is performed when ch
is read using select
in goroutine and when quit
notification comes. http://play.golang.org/p/_M6BjW2tNZ
package main
import "fmt"
import "time"
func routine() {
quit := make(chan bool)
defer close(quit)
ch := make(chan int)
go func() {
defer fmt.Println("end of iterator")
i := 0
for {
select {
case <-quit:
return
case ch <- i:
i++
}
}
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
func main() {
routine()
time.Sleep(time.Second)
}
However, even with this writing method, if you forget to close(quit)
, goroutine will easily leak, and
I don't think Go recommends implementing such an iterator with channels and goroutine.
Also, because the channel is slow, it can compromise execution speed, which is one of the great benefits of writing programs in Go.
If the internal state of the iterator is relatively simple (one int
in the question example), the loop end state is rewritten by receiving the pointer of the internal state as the receiver or argument of the next function. I think the correct way to write in Go is to return false
when it reaches. http://play.golang.org/p/8X0GjCOrMA
package main
import "fmt"
type IntRange struct {
cur int
end int
}
func NewIntRange(n int) *IntRange {
return &IntRange{
cur: -1,
end: n,
}
}
func (r *IntRange) Next() bool {
r.cur++
return r.cur < r.end
}
func main() {
sum := 0
r := NewIntRange(100)
for r.Next() {
if r.cur > 10 {
break
}
fmt.Println("val ==", r.cur)
sum += r.cur
}
fmt.Println("sum =", sum)
}
Or maybe the contents of the loop are passed using a callback. With this method, the definition side of the loop is simpler than the above code, but the user code is complicated. http://play.golang.org/p/7N8Wp3Q4Kj
package main
import "fmt"
func intRangeLoop(n int, body func(i int) bool) {
for i := 0; i < n; i++ {
if !body(i) {
break
}
}
}
func main() {
sum := 0
intRangeLoop(100, func(i int) bool {
if i > 10 {
return false
}
fmt.Println("val ==", i)
sum += i
return true
})
fmt.Println("sum =", sum)
}
Also, if you compare the execution speed with a simple program, you can see that the implementation using the channel is 100 times slower. http://play.golang.org/p/keunicUsHr (Go Playground cannot measure the execution time, so build and execute it at hand). Therefore, I think it is not recommended in Go today to implement iterators using channels.
Channel Version: 4.372033357s
Next version: 34.967099ms
Closure version: 43.641982ms