2996


GoでLuaのコルーチンやRubyのFiberのような機能を再現したいのですが、良い方法が思いつきません。

以下のようにGoroutineを使うと、似たようなことは可能ですが問題があります。

[code]

    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)
    }
    
[/code]

LuaやRubyでは、resumeできる状態であってもコルーチンはGCの対象になります。
しかし、Goではブロック中のGoroutineがGCの対象にならないため、メモリリークが発生してしまいます。

以下の例では、`ch`はGoroutineの中でしか参照されないためブロックの解除は永久に発生しない状況ですが、Goroutineは残ったままです。

[code]

    package main
    
    import (
        "runtime"
        "fmt"
    )
    
    func main(){
        for {
            go func(ch <-chan int) {
                <-ch
            }(make(chan int))
            runtime.GC()
            fmt.Println(runtime.NumGoroutine())
        }
    }
    
[/code]

何か良い解決方法はないでしょうか。


CC BY-SA 3.0
2015-01-03T07:59:21.847
2970
2015-01-10T15:17:51.673
3639
question
go
Goでコルーチンを再現する方法


go の goroutine は、与えた関数が終了しないかぎりGC対象にはなりません。 なのでgoroutineをGCしたいのであれば、
最初の例では無限ループではなく何かしらの終了フラグを用意してください。 2番めの例では渡したチャンネルを閉じてください。

Lua のコルーチンや Ruby の Fiber は、それが定義・保持されたスコープという寿命を持ちます。 そのため暗黙的に回収できるのです。 しかし
goroutine は関数の実行が終了するか、 親となったプロセス/main が終了するまで、寿命は継続し存続します。
つまり回収するためには明示的に終了する必要があるのです。

なお前者のようなカウンター的なものをコルーチンと呼んで良いのであれば、 なにも goroutine
を使わずに、以下の様な関数オブジェクトで十分なのではないでしょうか。 これであれば `newCounter` から返された関数は、
スコープなどから参照されなくなった瞬間にGC対象となります。

[code]

    package main
    
    import "fmt"
    
    func newCounter() func() int {
        i := 0
        return func() int {
            r := i
            i++
            return r
        }
    }
    
    func main() {
        c := newCounter()
        fmt.Println(c())
        fmt.Println(c())
        fmt.Println(c())
    }
    
[/code]

このソースは次で試せます: <https://play.golang.org/p/Wq5-CjEDtm>


CC BY-SA 3.0
2015-01-03T13:22:38.047
2993
2015-01-03T13:22:38.047
208
2970
answer


トリッキーなので使いどころが限定されますが、自分でもひとつ思いつきました。  
コルーチンの終了は手動ですが、コルーチンが破棄された時点でYield()内でpanicを発生させGoroutineを中断させます。

コルーチンをGCさせるのにGoroutineを最後まで実行しなくて済む、関数のネストに対応できるという点での再現度は高いと思います。

<http://play.golang.org/p/NSIlZ327sC>

[code]

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    var errResumeDeadFiber = errors.New("resume dead fiber")
    
    type fiberDeadPanic struct{}
    
    type Fiber struct {
        resume chan int
        close  chan int
    }
    
    func NewFiber() *Fiber {
        f := Fiber{
            resume: make(chan int),
            close:  make(chan int),
        }
        go f.run()
        return &f
    }
    
    func (f *Fiber) handleBreak() {
        if r := recover(); r != nil {
            if _, ok := r.(fiberDeadPanic); !ok {
                panic(r)
            }
        }
        fmt.Println("exit goroutine")
    }
    
    func (f *Fiber) run() {
        defer f.handleBreak()
    
        i := 0
        for {
            f.Yield(i)
            f.Yield(i)
            i++
        }
    }
    
    func (f *Fiber) Yield(i int) {
        select {
        case <-f.close:
            panic(fiberDeadPanic{})
        case f.resume <- i:
        }
    }
    
    func (f *Fiber) Resume() (int, error) {
        select {
        case <-f.close:
            return 0, errResumeDeadFiber
        default:
            return <-f.resume, nil
        }
    }
    
    func (f *Fiber) Close() {
        select {
        case <-f.close:
        default:
            close(f.close)
        }
    }
    
    func main() {
        for i := 0; i < 5; i++ {
            f := NewFiber()
            fmt.Println(f.Resume())
            fmt.Println(f.Resume())
            fmt.Println(f.Resume())
            fmt.Println(f.Resume())
            f.Close()
            fmt.Println(f.Resume())
        }
    }
    
[/code]


CC BY-SA 3.0
2015-01-03T14:16:32.330
2994
2015-01-03T14:28:13.467
2015-01-03T14:28:13.467
3639
3639
2970
answer


質問の "コルーチン" がコルーチンが一般に表す概念である

  * ユーザ (プログラマ) が `yield` を明示的に呼ぶことである時点で処理を中断して他の処理にコントロールを移行する仕組み
  * 逆にユーザが明示しない限りは処理が切り替わることはない, non-preemptiveな並行処理の仕組み

のことを指すのであれば、goroutine
はユーザが処理の切り替わりのタイミングや次に実行されるべき処理を明示的に指定することはできないので"コルーチン"ではないですし、コルーチンを実現するには現在の処理の実行状態(スタックとかコードの実行位置など)をどこかに保存する仕組みが必要なのでそのような仕組みが処理系に用意されていなければユーザがライブラリをいくら工夫しても実装できないので、Go
では"コルーチン"は実現できないというのが答えかと思います。

もし質問のコルーチンの意図が、「関数の実行を途中で中断することでイテレータを簡単に実装する方法」(例えば Python の `yield`)
でイテレータを中断してもメモリリークが起こらない方法を意図するのであれば、ご指摘のように[goroutineはGCの対象にならない](https://groups.google.com/d/msg/golang-
nuts/uiySuH8_3Y4/nvZoN7l0PxgJ)ので、イテレーターが内部で使っているgoroutineに停止用の信号を送ってやりgoroutineを明示的に停止するしかないと思います。

以下の例では終了通知用のチャネル `quit` を作り、goroutine内で `select` を使って `ch` が読み込まれた場合と `quit`
の通知が来た場合の分岐を行っています。<http://play.golang.org/p/_M6BjW2tNZ>

[code]

    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)
    }
    
[/code]

ただこの書き方でも `close(quit)` を忘れると簡単にgoroutineがリークしてしまいますし、  
Go ではチャネルとgoroutineを使ったこのようなイテレータの実装は推奨されていないように思います。  
またチャネルは遅いので、Goでプログラムを書く大きなメリットの1つである実行速度が損なわれる可能性があります。

質問の例のようなイテレータの内部状態が比較的シンプル (質問の例では `int` 1つ)
な場合なら内部状態のポインタをnext関数のレシーバか引数で受け取って内部状態を書き換えつつ、ループの終了状態に達したら `false`を返すのが Go
での正しい書き方かなと思います。<http://play.golang.org/p/8X0GjCOrMA>

[code]

    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)
    }
    
[/code]

あるいはループの中身をコールバックを使って渡すかですね。この方法だとループの定義側は上のコードよりシンプルになりますが、逆に利用側のコードが複雑になります。<http://play.golang.org/p/7N8Wp3Q4Kj>

[code]

    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)
    }
    
[/code]

また簡単なプログラムで実行速度を比べてみるとチャネルを使った実装は100倍以上遅いことが分かります。<http://play.golang.org/p/keunicUsHr>
(Go Playground では実行時間が測定できないので手元で build
して実行して下さい)。そのため、やはりチャネルを使ってイテレータを実装するのは現在のGoでは推奨されないかと思います。

[code]

    Channel Version: 4.372033357s
    Next version: 34.967099ms
    Closure version: 43.641982ms
    
[/code]


CC BY-SA 3.0
2015-01-03T14:50:59.437
2996
2015-01-10T15:17:51.673
2015-01-10T15:17:51.673
5575
5575
2970
answer


[tomb](http://gopkg.in/tomb.v2) というパッケージがあります。

> The tomb package handles clean goroutine tracking and termination. A Tomb
> tracks the lifecycle of one or more goroutines as alive, dying or dead, and
> the reason for their death.

[ソースコード](https://github.com/go-tomb/tomb/tree/v2)を読めばお判りかと思いますが、 h2so5
さんが回答されたものと内部構造は同じです。以下は質問欄の最初のコードで tomb パッケージを使ってみた例です。

[code]

    package main
    
    import (
        "fmt"
        "runtime"
    
        "gopkg.in/tomb.v2"
    )
    
    func main() {
        fmt.Printf("Number of goroutines = %d\n", runtime.NumGoroutine())
    
        tb := &tomb.Tomb{}
        ch := make(chan int)
    
        tb.Go(func() error {
            i := 0
            for {
                select {
                case <-tb.Dying():
                    close(ch)
                    return nil
                case ch <- i:
                    i++
                }
            }
        })
    
        fmt.Println("New goroutine starts.")
        fmt.Printf("Number of goroutines = %d\n", runtime.NumGoroutine())
    
        fmt.Printf("i = %d\n", <-ch)
        fmt.Printf("i = %d\n", <-ch)
        fmt.Printf("i = %d\n", <-ch)
    
        fmt.Println("Kill this goroutine.")
        tb.Kill(nil)
        tb.Wait()
        fmt.Println("This goroutine is dead.")
    
        runtime.GC()
        fmt.Println("Garbage collection is done.")
        fmt.Printf("Number of goroutines = %d\n", runtime.NumGoroutine())
    }
    
[/code]


CC BY-SA 3.0
2015-01-05T00:47:50.697
3083
2015-01-05T00:47:50.697
2970
answer


> LuaやRubyでは、resumeできる状態であってもコルーチンはGCの対象になります。
> しかし、Goではブロック中のGoroutineがGCの対象にならないため、メモリリークが発生してしまいます。

メモリリークは発生しません。

**追記**

gc で chan を閉じたいという事であれば、それは出来ません。goroutine を終了する為に chan を閉じて下さい。参照を保持しておいて、gc
前に close するのが良いと思います。

[code]

    package main
    
    import (
        "runtime"
        "fmt"
        "sync"
    )
    
    var m sync.Mutex
    var chs []chan int
    
    func main(){
        for {
            go func(ch chan int) {
                m.Lock()
                chs = append(chs, ch)
                m.Unlock()
                <-ch
            }(make(chan int))
    
            m.Lock()
            for _, ch := range chs {
                close(ch)
            }
            chs = []chan int{}
            m.Unlock()
    
            runtime.GC()
            fmt.Println(runtime.NumGoroutine())
        }
    }
    
[/code]


CC BY-SA 3.0
2015-01-05T02:23:52.330
3090
2015-01-05T03:14:59.187
2015-01-05T03:14:59.187
440
440
2970
answer