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
オプションを指定することで、実行中の競合検出を行うことができる。
参考文献
- 例えば java も同様の理由で
java.util.HashMap
などは非スレッドセーフ。ただし初期の頃の動的配列などはそうではなく、同期的なjava.util.Vector
のみが提供されていた。これも後に非同期的なjava.util.List
具象型(ArrayList
など)に置き換えられた。同期的動的配列はCollections.synchronizedList(new ArrayList<>())
でラップする [return]