おひとり

できる限りひとりで楽しむための情報やプログラミング情報など。

Goによる排他制御 ~ RWMutexによるRLockとLock

Go言語でMutexおよびRWMutexを使った排他制御について調べたのでまとめます。

RWMutexとMutex

Mutexを使ったLockは読み込み(Read)も書き込み(Write)も両方禁止できる。つまり他のゴルーチンがそのMutexによりLockされている資源を使いたい時は、直前のLockが解放されるまで待つことになる。
一方で、RWMutexはMutexの機能のほか、読み込み(Read)のみ許可するLockができる。 つまり、他のゴルーチンがRWMutexの読み込みロックされている資源を使いたいとき、書き込みについては待たされるが、読み込みだけであれば、直前のLockの解放を待たずに処理を実行できる。

つまり、RWMutexはMutexの機能も兼ねているということ。

RWMutexによる書き込み用ロック

書き込み用ロックでは、読み込みと書き込みの両方をLockできる。
これはMutexと同じ機能。

f:id:hitoridehitode:20200620003800p:plain
書き込み用ロック(Lock)では、完全に排他的に実行可能。

以下の例は資源abを排他制御により書き換える例。

package main

import (
    "fmt"
    "sync"
)

func main() {
    a := 5
    b := 3

    modifier := func(wg *sync.WaitGroup, l sync.Locker) {
        l.Lock()
        defer l.Unlock()
        defer wg.Done()
        a++
        b++
        fmt.Printf("a=%v, b=%v\n", a, b)
    }

    wg := sync.WaitGroup{}
    mu := sync.RWMutex{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go modifier(&wg, &mu)  // 書き込み用ロックではRWMutexの変数をそのまま渡せばよい。
    }
    wg.Wait()
    fmt.Println("Done")
}

上記のコードを実行すると、以下のような出力が得られる。

a=6, b=4
a=7, b=5
a=8, b=6
a=9, b=7
a=10, b=8
a=11, b=9
a=12, b=10
a=13, b=11
a=14, b=12
a=15, b=13
Done

書き込みと読み込みの両方がLockされるため、生成されたゴルーチンは排他的に実行される。

RWMutexによる読み込み用ロック

読み込み用ロックでは、他のゴルーチンが資源をLockしていても、それがRLock(読み込み用ロック)である場合は、当該Lockが解放されるのを待たずに実行できる。

f:id:hitoridehitode:20200620003723p:plain
RLockは並列に実行可能。その間、Lock勢は待たされる。

package main

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

func main() {
    a := 5
    b := 3

    reader := func(wg *sync.WaitGroup, l sync.Locker) {
        l.Lock()
        defer l.Unlock()
        time.Sleep(1 * time.Second) // 時間のかかる処理だとみなす
        fmt.Printf("a=%v, b=%v\n", a, b)
        defer wg.Done()
    }

    wg := sync.WaitGroup{}
    mu := sync.RWMutex{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go reader(&wg, mu.RLocker()) // RLocker()で読み込み用のLockerを取得
    }
    wg.Wait()
    fmt.Println("Done")
}

今回の例では、関数reader()は実行におよそ1秒かかる。(time.Sleep(1 * time.Second)のため)
そのため、通常のMutexでは単純に全ての処理が終わるまで10秒かかることに。
しかしこの例ではRWMutex.RLockerにより読み込み用のLockをしているため、他のRLockerのUnlockを待たずに実行可能。

よって、10回ゴルーチン(reader())が実行されるものの、およそ1秒(1回分の呼び出しと同じ時間)で終了。
読み出しのみであれば、RWMutexによるRLocker(RLock)を用いることにより、無駄な待ち時間を削減できる。

ただし、「読み込みロック中に共有資源の書き換えを行わない」という決まりはプログラマの責任で守るべきものであることに注意する。

RLockとLock

読み込み用ロックと書き込み用ロックを使った例。

  • modifier()reader()が読み込み用ロック(RLock)している間は待たされる。
  • reader()は他のreader()が読み込み用ロック(RLock)している間も、待たずに実行できる。ただし、modifier()が書き込み用ロック(Lock)している間はそれが解放されるまで待つ。
package main

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

func main() {
    a := 5
    b := 3

    reader := func(wg *sync.WaitGroup, l sync.Locker) {
        l.Lock()
        defer l.Unlock()
        time.Sleep(1 * time.Second) // 時間のかかる処理だとみなす
        fmt.Printf("reader a=%v, b=%v\n", a, b)
        defer wg.Done()
    }

    modifier := func(wg *sync.WaitGroup, l sync.Locker) {
        l.Lock()
        defer l.Unlock()
        defer wg.Done()
        a++
        b++
        fmt.Printf("modifier a=%v, b=%v\n", a, b)
    }

    wg := sync.WaitGroup{}
    mu := sync.RWMutex{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go reader(&wg, mu.RLocker()) // RLocker()で読み込み用のLockerを取得
        if i%5 == 0 {
            wg.Add(1)
            go modifier(&wg, &mu)
        }
    }
    wg.Wait()
    fmt.Println("Done")
}

まとめ

MutexとRWMutexについて調べました。
並行処理を行う際に必要に応じて使えれば非常に強力。一方で、その管理はプログラマの責任のもと行わなければならず、注意が必要。

参考文献

www.amazon.co.jp

f:id:hitoridehitode:20191003205941j:plain:w100
Go