Posted at

Go言語の並行処理デザインパターン by Rob Pike 後編

More than 3 years have passed since last update.

前編の続きです。

前回のデザパタを、Google検索を例に適用しましょうという話。


Google 1.0

まずは並行処理が入っていない、そのまま逐次処理の例です。

ダミーの検索関数type Search func(query string) Resultを返す、fakeSearch関数を作っておきます。

ダミーの検索関数Searchは、time.Sleepで一定時間スリープして、検索しているフリをします。


fake.go

package fake

import (
"fmt"
"math/rand"
"time"
)

var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
)

type Search func(query string) Result

func fakeSearch(kind string) Search {
return func(query string) Result {
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
return Result(fmt.Sprintf("%s result for %q\n", kind, query))
}
}

type Result string


メイン関数です。Google関数にて、先ほどのダミー検索関数を逐次処理で3回呼び出しています。


google1/main.go

package main

import (
"runtime"
"time"
"math/rand"
"fmt"
"github.com/tfutada/robpike/google/fake"
)

func main() {

runtime.GOMAXPROCS(runtime.NumCPU())
rand.Seed(time.Now().UnixNano())

start := time.Now()
results := Google("golang")
elapsed := time.Since(start)
fmt.Println(elapsed)
fmt.Println(results)
}

func Google(query string) (results []fake.Result) {
results = append(results, fake.Web(query))
results = append(results, fake.Image(query))
results = append(results, fake.Video(query))
return
}



実行結果

/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google1/main.go

160.479531ms
[web result for "golang"
image result for "golang"
video result for "golang"
]

Process finished with exit code 0


Google 2.0

Goルーチンを使用して並行処理します。前編のFanInパターンを使用します。

ダミーの検索関数をそれぞれ無名関数でラップし、チャネル経由で結果を返します。


google2/main.go

func Google(query string) (results []fake.Result) {

c := make(chan fake.Result)
go func() { c <- fake.Web(query) }()
go func() { c <- fake.Image(query) }()
go func() { c <- fake.Video(query) }()
for i := 0; i < 3; i++ {
results = append(results, <-c)
}

return
}



実行結果

前回より早くなりました。一番検索結果が遅いものが、全体の処理時間になります。

/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google2/main.go

82.139627ms
[web result for "golang"
image result for "golang"
video result for "golang"
]

Process finished with exit code 0


Google 2.1

前回のtimeoutパターンで、ある一定時間経過したら、検索を打ち切る処理を入れます。


timeout/main.go

func Google(query string) (results []fake.Result) {

c := make(chan fake.Result)
go func() { c <- fake.Web(query) }()
go func() { c <- fake.Image(query) }()
go func() { c <- fake.Video(query) }()

timeout := time.After(80 * time.Millisecond)

for i := 0; i < 3; i++ {
select {
case s := <- c:
results = append(results, s)
case <- timeout:
fmt.Println("Timed out!")
return
}
}

return
}



実行結果

処理が80msで打ち切られ、image検索が実行されてないのがわかります。

/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google3/main.go

Timed out!
80.690454ms
[web result for "golang"
video result for "golang"
]

Process finished with exit code 0


Google 3.0

次のチューニングは、検索処理の冗長化を行うものです。

同じ検索処理を並行して複数実行し、一番早く帰ってきたものを正として使用します。


first/main.go

func main() {

runtime.GOMAXPROCS(runtime.NumCPU())
rand.Seed(time.Now().UnixNano())

start := time.Now()

results := First("golang",
fake.FakeSearch("replica 1"),
fake.FakeSearch("replica 2"),
fake.FakeSearch("replica 3"),
fake.FakeSearch("replica 4"),
fake.FakeSearch("replica 5")) // 注) fakeSearch -> FakeSearch

elapsed := time.Since(start)
fmt.Println(elapsed)
fmt.Println(results)
}

func First(query string, replicas ...fake.Search) fake.Result {

c := make(chan fake.Result)
searchReplica := func(i int) {c <- replicas[i](query)}
for i := range replicas {
go searchReplica(i) // クロージャだとうまくいかない。。。
}
return <-c
}

// 最初のfakeSearchを大文字に変えて、メイン関数から見れるようにしました。
func FakeSearch(kind string) Search {
// 同じ


実行結果

/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google3/main.go

2.422057ms
replica 2 result for "golang"

Process finished with exit code 0

これを踏まえて、先ほどのGoogle 2.1のタイムアウトとの合わせ技をして、時間のかかるレプリカは切り捨てることで、より効率的に検索できるわけです。