38
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Mutex の使い方

Last updated at Posted at 2017-08-10

理解したいプログラムに、Mutexが出てきた。理解していないので、結構なんじゃこれ?と思ったので調べてみた。

Go での非同期処理 その1

前回、非同期について調べた時に、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 みたいにデータとってとかどうするんだ? できたとしてもむっちゃ面倒くさそうだ、、、

*Go maps in action

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!

ロックをかけるメソッドを、ロックをかけるメソッドから呼ばない

そうすると、下記のようなものでもデッドロックが決まる。だから、デッドロック発生。getcount を呼んでいるから。

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のこの回のコードをきっちりと理解したいと思っているところです。

38
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?