理解したいプログラムに、Mutexが出てきた。理解していないので、結構なんじゃこれ?と思ったので調べてみた。
前回、非同期について調べた時に、Go では
メモリをシェアして、コミュニケーションをとるのではなく、コミュニケーションをとって、メモリをシェアする」
ということが言われていたので、基本、非同期処理は、channel
を使うんじゃなかったっけ?と考えていたが、そうではないらしい。場合によって使い分けるらしい。このことを学ぶには、このポストが最高だ。
どんなケースで使うかというと
When to use Channels: passing ownership of data, distributing units of work and communicating async results
When to use Mutexes: caches, state
チャネル:データのオーナーシップを渡すとき。分散環境での、Unit of Work (パターン)を使うときや、非同期の結果をコミュニケートするとき
Mutex:キャッシュやステート
らしい。ふーん。じゃあ書いてみよう。
Mutex のコード
書いてみると、そんな複雑じゃない。単なるロックのお話だ。試しに、コンカレント実行で、非同期では安全に使えない Map を使ってみよう。こうして、Mutex で守ってあげると、Map も非同期実行でも安全に使える。
package main
import (
"fmt"
"sync"
"time"
)
type ReliableMap struct {
sync.Mutex
dictionary map[string]string
}
func New() *ReliableMap {
return &ReliableMap{
dictionary: make(map[string]string),
}
}
func (m *ReliableMap) Set(key string, value string) {
m.Lock()
m.dictionary[key] = value
m.Unlock()
}
func (m *ReliableMap) Get(key string) string {
m.Lock()
value := m.dictionary[key]
m.Unlock()
return value
}
func main() {
m := New()
for i := 0; i < 100; i++ {
go m.Set("yamada", fmt.Sprintf("%d Yen", i))
}
time.Sleep(time.Second)
fmt.Println(m.Get("yamada"))
}
実行結果。うん。ちゃんと動いている。
$ go run main.go
99 Yen
なんで Channel じゃダメなのよ
というわけで、書き方はわかった。じゃあ、なんでチャネルじゃダメなのよ。
ちょいとコード書いてみる。
func SetByChannel(key string, value string, c chan map[string]string) {
m := <-c
m[key] = value
c <- m
}
:
c := make(chan map[string]string)
for j := 0; j < 100; j++ {
go SetByChannel("ushio", fmt.Sprintf("%d Yen", j+1), c)
}
result := <- c
fmt.Println(result["ushio"])
ふん。そんな難しくないじゃないか。実行。あれー、、、
goroutine 172 [chan receive]:
main.SetByChannel(0x10b4c69, 0x5, 0xc4200c23c0, 0x6, 0xc4200c0000)
/Users/ushio/Codes/gosample/src/github.com/TsuyoshiUshio/Mutex/code/main.go:34 +0x4e
created by main.main
/Users/ushio/Codes/gosample/src/github.com/TsuyoshiUshio/Mutex/code/main.go:49 +0x380
goroutine 173 [chan receive]:
main.SetByChannel(0x10b4c69, 0x5, 0xc4200c23d0, 0x6, 0xc4200c0000)
/Users/ushio/Codes/gosample/src/github.com/TsuyoshiUshio/Mutex/code/main.go:34 +0x4e
created by main.main
/Users/ushio/Codes/gosample/src/github.com/TsuyoshiUshio/Mutex/code/main.go:49 +0x380
:
そういえば、チャネルって、一方通行だよな。Map みたいにデータとってとかどうするんだ? できたとしてもむっちゃ面倒くさそうだ、、、
Map のところを見ても次のように書いている
Maps are not safe for concurrent use: it's not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated by some kind of synchronization mechanism. One common way to protect maps is with sync.RWMutex.
つまり、Channel がベストプラクティスではない。だからキャッシュとかそんなのは、Mutex でロック書いたらいいのか。
RWMutex
じゃあ、RWMutex は?と言うと、ロックをかけるのに、Read/Writeのロックを別にかけられる感じ。
package main
import (
"fmt"
"sync"
"time"
)
type ReliableMap struct {
sync.RWMutex
dictionary map[string]string
}
func New() *ReliableMap {
return &ReliableMap{
dictionary: make(map[string]string),
}
}
func (m *ReliableMap) Set(key string, value string) {
m.Lock()
m.dictionary[key] = value
m.Unlock()
}
func (m *ReliableMap) Get(key string) string {
m.RLock()
value := m.dictionary[key]
m.RUnlock()
return value
}
func main() {
m := New()
for i := 0; i < 100; i++ {
go m.Set("yamada", fmt.Sprintf("%d Yen", i))
}
time.Sleep(time.Second)
fmt.Println(m.Get("yamada"))
}
実行結果は同じだけど、多分こっちのほうが効率がいい
$ go run main.go
99 Yen
その他の Tips
先に紹介した、Dancing with Go’s Mutexesでは、とってもいいTips が紹介されている。
Unlock は defer を使うと良いけど、注意が必要。ここからは単なる上記のブログの引用です。
Lock は必要なところだけ
これあかん。I/O のような重い処理も入れてLock/Unlock() せずに一行上に上げて、http.Get() は、ロックの外で実施しよう。
func doSomething(){
mu.Lock()
item := cache["myKey"]
http.Get() // Some expensive io call
mu.Unlock()
}
Unlock に defer はいい感じ。
func doSomething(){
mu.Lock()
defer mu.Unlock()
err := ...
if err != nil{
//log error
return // <-- your unlock will happen here
}
err = ...
if err != nil{
//log error
return // <-- or here here
}
return // <-- and of course here
}
でもこれだと DeadLock
なぜなら、defer の時には、スコープの外に出ているから。
func doSomething(){
for {
mu.Lock()
defer mu.Unlock()
// some interesting code
// <-- the defer is not executed here as one *may* think
}
// <-- it is executed here when the function exits
}
// Therefore the above code will Deadlock!
ロックをかけるメソッドを、ロックをかけるメソッドから呼ばない
そうすると、下記のようなものでもデッドロックが決まる。だから、デッドロック発生。get
がcount
を呼んでいるから。
package main
import (
“fmt”
“sync”
)
type DataStore struct {
sync.Mutex // ← this mutex protects the cache below
cache map[string]string
}
func New() *DataStore{
return &DataStore{
cache: make(map[string]string),
}
}
func (ds *DataStore) set(key string, value string) {
ds.Lock()
defer ds.Unlock()
ds.cache[key] = value
}
func (ds *DataStore) get(key string) string {
ds.Lock()
defer ds.Unlock()
if ds.count() > 0 { <-- count() also takes a lock!
item := ds.cache[key]
return item
}
return “”
}
func (ds *DataStore) count() int {
ds.Lock()
defer ds.Unlock()
return len(ds.cache)
}
func main() {
/* Running this below will deadlock because the get() method will take a lock and will call the count() method which will also take a lock before the set() method unlocks()
*/
store := New()
store.set(“Go”, “Lang”)
result := store.get(“Go”)
fmt.Println(result)
}
どうするかというと、ロックをかけるメソッドとそうでないのを分けてリファクタリングする
package main
import (
“fmt”
“sync”
)
type DataStore struct {
sync.Mutex // ← this mutex protects the cache below
cache map[string]string
}
func New() *DataStore {
return &DataStore{
cache: make(map[string]string),
}
}
func (ds *DataStore) set(key string, value string) {
ds.cache[key] = value
}
func (ds *DataStore) get(key string) string {
if ds.count() > 0 {
item := ds.cache[key]
return item
}
return “”
}
func (ds *DataStore) count() int {
return len(ds.cache)
}
func (ds *DataStore) Set(key string, value string) {
ds.Lock()
defer ds.Unlock()
ds.set(key, value)
}
func (ds *DataStore) Get(key string) string {
ds.Lock()
defer ds.Unlock()
return ds.get(key)
}
func (ds *DataStore) Count() int {
ds.Lock()
defer ds.Unlock()
return ds.count()
}
func main() {
store := New()
store.Set(“Go”, “Lang”)
result := store.Get(“Go”)
fmt.Println(result)
}
終わりに
今回は、自分では、Map でチャネルのサンプルを書いてめんどくささを実感したかったのですが、うまくかけませんでした。(Mutexを使う充分な動機ですが)もし、うまくかける人がいたらぜひご教授を
さて、まだこれはヤクの毛狩りの途中です。go-in-5-minutesのこの回のコードをきっちりと理解したいと思っているところです。