golang同時ロックの罠
3168 ワード
エラーコードの例
package main
import (
"sync"
"strconv"
"fmt"
)
type Node struct {
sync.Mutex
Data map[string]string
}
var Cache []Node;
func main() {
Cache = make([]Node, 2);
Cache[0] = Node{Data : make(map[string]string)}
Cache[1] = Node{Data : make(map[string]string)}
wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
wg.Add(1)
go func (index int) {
defer wg.Done()
j := index % 2
node := Cache[j]
node.Lock()
defer node.Unlock()
node.Data[strconv.Itoa(index)] = strconv.Itoa(index)
}(i)
}
wg.Wait();
fmt.Println(Cache[0])
}
上記のコードロジックを見ると簡単で、10,000個の協程を併発してCacheのデータに値を付与し、偶数
index
は0
個目のmapに値を付与し、奇数は1
個目のmapに値を付与し、mapは値を付与する際にロックをかけたが、golang 1.8が実行されている間に以下のエラーが爆発するfatal error: concurrent map writes
fatal error: concurrent map writes
goroutine 26 [running]:
runtime.throw(0x10b4392, 0x15)
......
なぜロックをかけてもcuncurrent map wirtesを報告するのか、これはgolang 1.8のバグに違いない(冗談だ......)!
エラーの原因
主な原因はgolangのstructが値を付与する時に浅いコピーを行って、構造体のメンバーをcopyに行って、Node構造体は2つのメンバーがあります
type Node struct {
sync.Mutex
Data map[string]string
}
私たちがsliceからノードを取り出したとき、実はcopyでノードが1部、Mapはポインタタイプだったので、複数のcopyは実はmapを操作していましたがsync.Mutexタイプはstructで、彼は1回copyを行ったので、各協程で取り出したとき、Mutexは1回copyを行ったが、ロックの時は同じロックではないので、同時map書き込みが発生する.
解決方法1
NodeのメンバーMutexをポインタタイプに変更すると、copyのとき、mutexは同じ部分をロックすることができます.コードは以下の通りです.
package main
import (
"fmt"
"strconv"
"sync"
)
type Node struct {
*sync.Mutex
Data map[string]string
}
var Cache []Node
func main() {
Cache = make([]Node, 2)
Cache[0] = Node{Data: make(map[string]string), Mutex: &sync.Mutex{}}
Cache[1] = Node{Data: make(map[string]string), Mutex: &sync.Mutex{}}
wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
j := index % 2
node := Cache[j]
node.Lock()
defer node.Unlock()
node.Data[strconv.Itoa(index)] = strconv.Itoa(index)
}(i)
}
wg.Wait()
fmt.Println(Cache[0])
}
Mutexをポインタタイプに変更すると、同じロックが保証されます.
解決策2 CacheにNodeポインタを格納
CacheでNodeポインタタイプの場合、indexアクセス時にポインタのコピーを取り出し、同じアドレスを指し、ロック時に同じリソースコードにアクセスするのは以下の通りです.
package main
import (
"fmt"
"strconv"
"sync"
)
type Node struct {
sync.Mutex
Data map[string]string
}
var Cache []*Node
func main() {
Cache = make([]*Node, 2)
Cache[0] = &Node{Data: make(map[string]string)}
Cache[1] = &Node{Data: make(map[string]string)}
//fmt.Println(Cache);return;
wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
j := index % 2
node := Cache[j]
node.Lock()
defer node.Unlock()
node.Data[strconv.Itoa(index)] = strconv.Itoa(index)
}(i)
}
wg.Wait()
fmt.Println(Cache[0])
}
まとめ
golangはC++に類似しており,システムが提供する付与値はいずれも浅いコピーであり,同じコンテンツへのアクセスが必要であることを確認した場合,特定の場所でポインタを用いる必要がある.