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のデータに値を付与し、偶数index0個目の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++に類似しており,システムが提供する付与値はいずれも浅いコピーであり,同じコンテンツへのアクセスが必要であることを確認した場合,特定の場所でポインタを用いる必要がある.