Go

syncパッケージのOnce, WaitGroup, Mutex

More than 3 years have passed since last update.

はじめに

Githubで公開されているソースコードを読みながら、Go言語を勉強しています。

Go言語には並列処理の機能があります。

Githubで公開されているソースコードを読むために、Go言語の並列処理について学ぶ必要がありました。

Go言語には同期処理、排他制御などの機能を提供する便利なsyncパッケージがあります。

今回はこの便利なsyncパッケージのOnce, WaitGroup, Mutexについて書きたいと思います。

この記事ではGo言語のインストール方法や構文などの基本的なことについては触れません。もし、それらに興味があるならば、こちらの記事が参考になると思います。

また、Go言語の並列処理に関する gorutine, channel, select, close などについてはこちらの記事にわかりやすくまとまっています。

sync.Once

まずは、sync.Onceです。関数を一度だけ呼び出したい場合に使用します。

once_sample.go
package main

import "fmt"
import "sync"

func main() {
    var once sync.Once

    once.Do(func() {fmt.Println("A")})
    once.Do(func() {fmt.Println("B")}) // こちらの関数は呼び出されない
}

こちらはGo Playgroundへのリンクです。

余談ですが、play goと検索したところ、打ちながら囲碁のルールと基本的なテクニックを学ぶことができるサイトを見つけました。

sync.WaitGroup

sync.WaitGroupについてはこちらの記事にわかりやすくまとまっています。

自分のために少しまとめておきます。

同期待ちにsync.WaitGroupを使用します。

sync.WaitGroupには、定石があるように思います。

それはgorutineの起動前にAdd(1)、gorutineの終了前にDone()、同期待ちにWait()という定石です。

wait_sample.go
package main

import "fmt"
import "sync"

func main() {
    var wait sync.WaitGroup

    wait.Add(1) // gorutineの起動前
    go func() {
        defer wait.Done() // gorutineの終了前
        fmt.Println("A")
    }()

    wait.Add(1) // gorutineの起動前
    go func() {
        defer wait.Done() // gorutineの終了前
        fmt.Println("B")
    }()

    wait.Wait() // 同期待ち
}

こちらはGo Playgroundへのリンクです。

wait_sample.go

sync.Mutex

排他制御にsync.Mutexを使用します。

case1

まずはsync.Mutexを使用しない場合を見てみます。

case1.go
package main

import "fmt"
import "sync"

func main() {
    c := 0
    var w sync.WaitGroup

    for i := 0; i < 300; i++ {
        w.Add(1)
        go func() {
            defer w.Done()
            c++
        }()

        w.Add(1)
        go func() {
            defer w.Done()
            fmt.Println(c)
        }()
    }

    w.Wait()
}

期待する結果は1から300が出力されることです。

期待しない結果になることを確認します。

こちらはGo Playgroundへのリンクです。

case1.go

同じ数字が出力されていること、最後が300でないことから期待しない結果だとわかります。

case2

なにが起こったのか確認します。

case2.go
package main

import "fmt"
import "sync"

func main() {
    c := 0
    var w sync.WaitGroup
    var m sync.Mutex

    for i := 0; i < 300; i++ {
        w.Add(1)
        go func() {
            defer w.Done()
            m.Lock()
            c++
        }()

        w.Add(1)
        go func() {
            defer w.Done()
            m.Unlock()
            fmt.Println(c)
        }()
    }

    w.Wait()
}

こちらはGo Playgroundへのリンクです。

case2.go

実行中にパニックが生じます。

panic: sync: unlock of unlocked mutex

前者のgorutineでロックして、後者のgorutineでアンロックしています。

後者のgorutineが前者のgorutineを追い越し、アンロックしたミューテックスを、ロックする前に、再度アンロックしたようですね。

case3

つぎにsync.Mutexを使用する場合を見てみます。

case3.go
package main

import "fmt"
import "sync"

func main() {
    c := 0
    var w sync.WaitGroup
    var m sync.Mutex

    for i := 0; i < 300; i++ {
        w.Add(1)
        m.Lock()
        go func() {
            defer w.Done()
            defer m.Unlock()
            c++
        }()

        w.Add(1)
        m.Lock() // ロック中ならば、アンロックを待つ
        go func() {
            defer w.Done()
            defer m.Unlock()
            fmt.Println(c)
        }()
    }

    w.Wait()
}

こちらはGo Playgroundへのリンクです。

case3.go

期待する結果です。

実装でこちらを参照しました。

私の扱った例が悪かったかもしれません。

期待する結果ですが、すでに、並列処理になっていませんね。。。

まとめ

syncパッケージについてはこちらの記事にわかりやすくまとまっています。

いろいろと試した結果をわかりやすくまとめたつもりです。

sync.Mutexで扱った例は少し適切ではなかったかもしれませんが、参考になれば幸いです。