Go
Design
concurrent
pattern

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

More than 1 year has 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のタイムアウトとの合わせ技をして、時間のかかるレプリカは切り捨てることで、より効率的に検索できるわけです。