Golang Cafe #9 まとめ Go言語でデザインパターン

2013/12/22に開催された「Golang Cafe #9」についてのまとめです。
本来は参加する予定ではなかったのですが、時間がとれたので参加しました。

今回は

の内容をGo言語で書いてみるというものでした。
サンプルコード+TakashiYokoyama氏が用意してくれました。
時間にも限りがあり、singlethreadexecution、immutable、およびguardedsuspensionの3つのみの検証となりました。

singlethreadexecution

複数スレッドから同じ値を参照すると整合性がとれなくなってしまうというもの。
メイン関数にruntime.GOMAXPROCS(2)を追加して実行すると下記のように表示されます。
(私のノートPCで実行すると下記のように出力されるのですが、私のデスクトップやMacチームは下記のような結果にはなりませんでした)

$ go run main.go
Testing Gate, hit CTRL+C to exit.
Alice BEGIN
Bobby BEGIN
Chris BEGIN
***** BROKEN ***** No.2157: Chris, Canada
***** BROKEN ***** No.2249: Bobby, Brazil
***** BROKEN ***** No.8642: Bobby, Brazil
***** BROKEN ***** No.10887: Chris, Canada
***** BROKEN ***** No.11187: Bobby, Brazil
***** BROKEN ***** No.13887: Chris, Canada
***** BROKEN ***** No.15086: Alice, Alaska
***** BROKEN ***** No.17209: Chris, Canada

これを防ぐにはどうしたらいいのか、参加者それぞれ方法がありましたが、私が一番スマートだと思った方法はこれです。


Gateにflagを追加します。

type Gate struct {
    counter int
    name string
    address string
    flag chan bool
}

Start関数のforループ内を修正します。

func(thread UserThread) Start() {
    println(fmt.Sprintf("%s BEGIN", thread.myName))
    for {
        <-thread.gate.flag
        thread.gate.Pass(thread.myName, thread.myAddress)
        time.Sleep(1 * time.Millisecond)
        thread.gate.flag <- true
    }
}

main関数のgate初期化のあとにflagを初期化します。

func main() {
    runtime.GOMAXPROCS(2)

    println("Testing Gate, hit CTRL+C to exit.")

    gate := Gate {counter: 0, name: "Nobody", address: "Nowhere"}
    gate.flag = make(chan bool, 1)
    gate.flag <- true

    // 以下は省略
}

Gate自体にバッファ1のチャンネル変数を追加することによりGateへの同時アクセスを制御しようというものです。
MutexのLock、Unlockを使用しても制御できそうですがこの方がスマートに思えました。


これで実行すると、下記のように正常に表示されます。

$ go run main.go
Testing Gate, hit CTRL+C to exit.
Alice BEGIN
Bobby BEGIN
Chris BEGIN


チャンネル変数はgoroutine間の値の受け渡しに使うだけでなく、バッファを持たせることでロックの代わりに使用できる典型的な例です。

immutable

実行結果の一例ですが下記のような内容続きます。

$ go run main.go
3 prints [ Person: name = Alice, address = Alaska ]1 prints [ Person: name = Alice, address = Alaska
 ]2 prints [ Person: name = Alice, address = Alaska ]


3 prints [ Person: name = Alice, address = Alaska ]
1 prints [ Person: name = Alice, address = Alaska ]2 prints [ Person: name = Alice, address = Alaska
 ]3 prints [ Person: name = Alice, address = Alaska ]

先ほどのサンプルでは一つの値に対して変更できる術があったがために不整合が起きてしまいましたが、このサンプルのように変更する方法がない場合には当然不整合は起きないというものです。

guardedsuspension

まず実行結果ですが以下のようになります。

$ go run main.go
ClientThread requests [ Request No. 0 ]
ServerThread handles [ Request No. 0 ]
ClientThread requests [ Request No. 1 ]
getRequest wait
ServerThread handles [ Request No. 1 ]
getRequest waitClientThread requests [ Request No. 2 ]

ServerThread handles [ Request No. 2 ]
getRequest waitClientThread requests [ Request No. 3 ]

ServerThread handles [ Request No. 3 ]
getRequest waitClientThread requests [ Request No. 4 ]

ServerThread handles [ Request No. 4 ]
getRequest waitClientThread requests [ Request No. 5 ]

ServerThread handles [ Request No. 5 ]

Clientのrequestに対してServerのhandle順次発生しています。


ポイントとなるのは以下の2つのメソッドです。

func (queue RequestQueue) getRequest() Request {
    queue.cond.L.Lock()
    if queue.queue.Len() <= 0 {
        // wait
        println("getRequest wait")
        queue.cond.Wait()
    }

    element := queue.queue.Front()
    request := element.Value.(Request)
    queue.queue.Remove(element)

    queue.cond.L.Unlock()
    return request
}
func (queue RequestQueue) putRequest(req Request) {
    queue.cond.L.Lock()
    queue.queue.PushBack(req)
    queue.cond.Signal()
    queue.cond.L.Unlock()
}

ClientがputRequestで値を設定したあとSignalメソッドを呼びます、するとgetRequestのWaitメソッドで待機していたServerの処理が再開されるというものです。
C#でいうとManualResetEventAutoResetEventのような使い方ですが、Waitメソッド、Signalメソッドを呼ぶ前後にLock、Unlockが必要なところに気をつけなくてはなりません。

簡単にまとめ

数ヶ月ほどGo言語に取り組んできましたが、やはり「goroutineとチャンネルをどう駆使するか」というところがポイントのように思えてきました。
まだまだ奥が深そうです。


年末と理由にしてはいけませんがまとめが遅くなってしまいました。