【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.RWMutex
は sync.Mutex
が提供する Lock()/Unlock()
のほか、 RLock()/RUnlock()
を提供する。
Lock()/Unlock()
は占有ロック。ロック時に対象への書き込み、読み込みを禁止する。RLock()/RUnlock()
は共有ロック。ロック時に対象への書き込みのみ禁止し、読み込みは禁止しない。
読み込み処理の場合、他ゴルーチンからの読み込み処理も考慮して RLock
を使用していくと良い。
Go の競合検出
Go ではコードの実行・ビルド時、あるいはパッケージインストール時に -race
オプションを指定することで、実行中の競合検出を行うことができる。