Go言語

【Go言語】syncパッケージのMutex/RWMutexの使い方

Mutexとは?

「Mutex(ミューテックス)」は「相互排他」を表す"mutual exclusion"の略で、クリティカルセクション(共有リソースに対する排他的なアクセスが必要な場所)を保護する方法。
syncパッケージで定義されている。

Mutexを使うことで、並行処理を実装する際に、安全に共有リソースに対する排他的アクセスを実現できる

これだけだと良くわからないのでサンプルコードを書いてみます。

Mutexのサンプルコード

2つのゴルーチンから、共通の変数をインクリメント・デクリメントする処理を書いてみる。

Mutexを使うことで共通変数countを「インクリメントするときには、デクリメントできない」といった相互排他制御が簡単に実装できる。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var count int
    var mu sync.Mutex

    increment := func() {
        mu.Lock()         // muをロックする
        defer mu.Unlock() // 処理完了したらmuのロック解除

        count++
        time.Sleep(1 * time.Second)
        fmt.Printf("Increment. count: %d\n", count)
    }

    decrement := func() {
        mu.Lock()
        defer mu.Unlock()

        count--
        time.Sleep(1 * time.Second)
        fmt.Printf("Decrement. count: %d\n", count)
    }

    var wg sync.WaitGroup
    // インクリメントするゴルーチンを5つ生成
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }

    // デクリメントするゴルーチンを5つ生成
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            decrement()
        }()
    }

    wg.Wait()
    fmt.Println("DONE!!")
}

Go Playground: https://play.golang.org/p/thnVw7_GShw

実行結果は次の通り。
インクリメントかデクリメントどちらかしか実行されていないことがわかる。

$ go run main.go
Increment. count: 1
Decrement. count: 0
Decrement. count: -1
Decrement. count: -2
Decrement. count: -3
Increment. count: -2
Increment. count: -1
Increment. count: 0
Decrement. count: -1
Increment. count: 0
DONE!!

ポイント

Unlockの呼び出しにはdeferを使うとよい!!

こうすることで、panicになったとしても確実に呼び出される。
呼び出し失敗したり、忘れたりするとデッドロックが発生してしまう。

 

RWMutexとは?

RWMutexは概念的にはMutexと同じもので、メモリへのアクセスを保護してくれる。
同じくsyncパッケージで定義されている。

Mutexとの違いは、RWMutexでは2種類のロックが提供されている点。

  • RLock : 読み取り用のロック。RLock同士はブロックせず、Lockのみがブロックされる。解除時は RUnlockを使う
  • Lock : Mutexと同じロック。RLock, Lock双方をブロックする。

RWMutexのサンプルコード

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var count int
    var mu sync.RWMutex

    increment := func() {
        mu.Lock()         // muをロックする
        defer mu.Unlock() // 処理完了したらmuのロック解除

        count++
        time.Sleep(1 * time.Second)
        fmt.Printf("Increment. count: %d\n", count)
    }

    decrement := func() {
        mu.Lock()
        defer mu.Unlock()

        count--
        time.Sleep(1 * time.Second)
        fmt.Printf("Decrement. count: %d\n", count)
    }
    // Readロック
    read := func() {
        mu.RLock()
        defer mu.RUnlock()
        time.Sleep(1 * time.Second)
        fmt.Printf("reading. count: %d\n", count)
    }

    var wg sync.WaitGroup
    // インクリメントするゴルーチンを5つ生成
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }

    // デクリメントするゴルーチンを5つ生成
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            decrement()
        }()
    }
    
    // 読み取りするゴルーチンを5つ生成
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            read()
        }()
    }
    wg.Wait()
    fmt.Println("DONE!!")
}

Go Playground: https://play.golang.org/p/OiC48KkJmwk

実行結果は次の通り。
RLock同士はブロックしないので、readがほぼ同時に実行されています。

reading. count: 0
Increment. count: 1
reading. count: 1
reading. count: 1
reading. count: 1
reading. count: 1
Increment. count: 2
Increment. count: 3
Decrement. count: 2
Increment. count: 3
Increment. count: 4
Decrement. count: 3
Decrement. count: 2
Decrement. count: 1
Decrement. count: 0
DONE!!

 

参考図書

Tour of Goを終えた人にオススメです!!

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語