Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Golang】Golang の map の非スレッドセーフ性と排他制御

Golang の map はスレッドセーフでない

Golang の map はスレッドセーフ、もといゴルーチンセーフではない。そのため、複数のゴルーチンからの同時アクセスによって整合性が保たれない状態になることがある。

例えば、以下のように map に対して複数のゴルーチンからの書き込みが発生すると、たまに非同期書き込み失敗のエラーが発生する。

package main

func main() {

    kvs := NewKeyValueStore()

    for i := 0; i < 10; i++ {
        go func(kvs *KeyValueStore) {
            kvs.set("key", "value")
            kvs.get("key")
        }(kvs)
    }

}

type KeyValueStore struct {
    m map[string]string
}

func NewKeyValueStore() *KeyValueStore {
    return &KeyValueStore{m: make(map[string]string)}
}

func (s *KeyValueStore) set(k, v string) {
    s.m[k] = v
}

func (s *KeyValueStore) get(k string) (string, bool) {
    v, ok := s.m[k]
    return v, ok
}

実行結果(たまに起こる)

fatal error: concurrent map writes

map がなぜデフォルトで非スレッドセーフになっているかというと、単純にパフォーマンス上不利であるためと思われる。この仕様のため、複数御ルーチンから map アクセスを実施するには明示的に排他制御を行う必要がある。1

map の排他制御

Go で排他制御を実現するには、sync.Mutex を使用する。

package main

import (
    "fmt"
    "sync"
)

func main() {

    kvs := NewConcurrentKeyValueStore()

    for i := 0; i < 10; i++ {
        go func(kvs *ConcurrentKeyValueStore) {
            kvs.set("key", "value")
            kvs.get("key")
        }(kvs)
    }

}

type ConcurrentKeyValueStore struct {
    m  map[string]string
    mu sync.RWMutex
}

func NewConcurrentKeyValueStore() *ConcurrentKeyValueStore {
    return &ConcurrentKeyValueStore{m: make(map[string]string)}
}

func (s *ConcurrentKeyValueStore) set(k, v string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[k] = v
}

func (s *ConcurrentKeyValueStore) get(k string) (string, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.m[k]
    return v, ok
}

上記のコードでは sync.RWMutex を使用している。sync.RWMutexsync.Mutex が提供する Lock()/Unlock() のほか、 RLock()/RUnlock() を提供する。

  • Lock()/Unlock() は占有ロック。ロック時に対象への書き込み、読み込みを禁止する。
  • RLock()/RUnlock() は共有ロック。ロック時に対象への書き込みのみ禁止し、読み込みは禁止しない。

読み込み処理の場合、他ゴルーチンからの読み込み処理も考慮して RLock を使用していくと良い。

Go の競合検出

Go ではコードの実行・ビルド時、あるいはパッケージインストール時に -race オプションを指定することで、実行中の競合検出を行うことができる。

Golang の並列実行時の競合状態検出

参考文献


  1. 例えば java も同様の理由で java.util.HashMap などは非スレッドセーフ。ただし初期の頃の動的配列などはそうではなく、同期的な java.util.Vector のみが提供されていた。これも後に非同期的な java.util.List 具象型( ArrayList など)に置き換えられた。同期的動的配列は Collections.synchronizedList(new ArrayList<>()) でラップする