やりたいこと
goで処理を並列化したいけれど、愚直にgoroutineを生成すると、数が膨大になってしまうケースの解決方法として、一定数のgoroutine数に制限しつつ、制限を突破した場合であっても待たずにメインスレッドで処理してしまいたい。
という、あるのかないのかわからない状況を想定する
goroutineの数を制限する
goroutineを制限する方法にはchannelを使う
// 生成できるgoroutineを100に制限する
ch := make(chan bool,100)
for i:=0; i<100; i++{
ch <- true
go func(){ someFunc(); <- ch }
}
実行中のgoroutineを取得
実行中のgoroutine数(channelの空き数)を取得するには、len(ch)を使えば取れそう。
リファレンスには
A single channel may be used in send statements, receive operations, and calls to the built-in functions cap and len by any number of goroutines without further synchronization. Channels act as first-in-first-out queues. For example, if one goroutine sends values on a channel and a second goroutine receives them, the values are received in the order sent.
としか書いていないが、lenでgoroutineの数をカウントするのがいいよ(=チャンネルに送られたデータの個数を表すよ)と書いてあると解釈して、また動作的にもそう動いているということは確認したが、念のため裏を取る
https://github.com/golang/go/blob/f47c8f130e4f5642cda5ee98741c2de25fde8b7e/src/runtime/chan_test.go
// Test len/cap.
c := make(chan int, chanCap)
if len(c) != 0 || cap(c) != chanCap {
t.Fatalf("chan[%d]: bad len/cap, expect %v/%v, got %v/%v", chanCap, 0, chanCap, len(c), cap(c))
}
for i := 0; i < chanCap; i++ {
c <- i
}
if len(c) != chanCap || cap(c) != chanCap {
t.Fatalf("chan[%d]: bad len/cap, expect %v/%v, got %v/%v", chanCap, chanCap, chanCap, len(c), cap(c))
}
chanのテストで、lenがチャネルにデータが格納されているデータ個数であることを確認しているので、信用してよいことにする。
サンプル
前置きに対してサンプルは単純
import (
"sync"
"fmt"
"time"
)
func main(){
ch := make(chan bool,100)
for i:=0; i<2000; i++{
if len(ch)<cap(ch){
ch <- true
go func(){fmt.Print("from goroutine:");someFunc(); <- ch}()
}else{
someFunc()
}
}
}
func someFunc(){
fmt.Println("someFunc is called")
time.Sleep(100*time.Millisecond)
}
これを実行すると、100個までgoroutineを生成して、そのあとは新規にgoroutineを作れないので、メインスレッドでsomeFuncを実行し、その間にほかのgoroutineが終了するので、空きができてまたgoroutineを生成できる。という処理の繰り返しになる。
本来やりたかったこと
これを調べた動機は、Fibonacci数列を求めるプログラムで、キャッシュテーブルを使用する実装の後に、処理をgoroutine化したらどうなるんだろう。という疑問が沸いて実装したけれど、goroutine数が膨大になったためエラーになる。という事案が発生したため、goroutine数を現実的な範囲に収めて、ベンチマークを取りたかったのでした。
package main
import "sync"
var (
fibTable map[int]int
mu sync.Mutex
ch = make(chan bool, 1000)
)
func fibWithTGoroutine(x int) int {
mu.Lock()
if fibTable == nil {
fibTable = map[int]int{}
}
if val, ok := fibTable[x]; ok {
mu.Unlock()
return val
}
mu.Unlock()
switch x {
case 0:
mu.Lock()
fibTable[0] = 0
mu.Unlock()
return 0
case 1:
mu.Lock()
fibTable[1] = 1
mu.Unlock()
return 1
}
ch1 := make(chan int)
ch2 := make(chan int)
retval := 0
fmt.Printf("len/cap = %d/%d\n", len(ch), cap(ch))
if len(ch) < cap(ch)-2 {
ch <- true
ch <- true
go func() { ch1 <- fibWithTGoroutine(x - 2); <-ch }()
go func() { ch2 <- fibWithTGoroutine(x - 1); <-ch }()
retval = <-ch1 + <-ch2
} else {
retval = fibWithTGoroutine(x-2) + fibWithTGoroutine(x-1)
}
mu.Lock()
fibTable[x] = retval
mu.Unlock()
return retval
}
キャッシュテーブル作ってるから並列化のメリットがない(むしろデメリットがある)のではという気はしていたけれど、ベンチマークを取ってみたところ案の定遅くなっていた。
BenchmarkFibT-4 2000000000 0.01 ns/op
BenchmarkFibTGoroutine-4 1 2743509100 ns/op
ということで、今回のケースでこの処理は意味がないという結論になった。
おまけ
意味がない。以上。だと寂しいので、並列化のメリットを出すために、このメソッド呼び出しには、10msecのペナルティがあるという想定の元ベンチマークを取ってみた。(メソッドの頭で10msec スリープするだけ)
BenchmarkFibT-4 1 16765166300 ns/op
BenchmarkFibTGoroutine-4 1 8596307500 ns/op
よかった。2倍程度早くなった