こんにちは。KOUKIです。
ここで紹介するコードは、以下の書籍からの引用です。
ここでは、RWMutexを紹介します。
<目次>
RWMutexとは
sync.RWMutexは、Mutexと概念的には同じもので、メモリのアクセスを保護します。
※メモリのアクセス保護とは、プロセスの中で書込や読込を一つのプロセスだけが行えるようにすることをここでは指しています
しかし、Mutexよりメモリに対する管理機構を多く提供しています。
複数の並行処理のプロセスで共有するメモリがあったとしても、そのプロセスの全てが書込や読込の両方を必要としているわけではないです。
RWMutexは、「メモリに対する読み込みのロックを要求した場合、ロックが書き込みで保持されていなければ、アクセスを得ることができる」などの制御が可能です。
引用コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package main import ( "fmt" "math" "os" "sync" "text/tabwriter" "time" ) func main() { // sync.Locker1はinterface // Mutex/RWMutex型を満たす producer := func(wg *sync.WaitGroup, l sync.Locker) { defer wg.Done() for i := 5; i > 0; i-- { l.Lock() // something work... time.Sleep(1) l.Unlock() } } observer := func(wg *sync.WaitGroup, l sync.Locker) { defer wg.Done() l.Lock() defer l.Unlock() // something work... } test := func(count int, mutex, rwMutex sync.Locker) time.Duration { var wg sync.WaitGroup wg.Add(count + 1) beginTestTime := time.Now() go producer(&wg, mutex) for i := count; i > 0; i-- { go observer(&wg, rwMutex) } wg.Wait() return time.Since(beginTestTime) } // Tableっぽく出力してくれる tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0) defer tw.Flush() var m sync.RWMutex // Table Header fmt.Fprintf(tw, "Readers\tRWMutex\tMutex\n") for i := 0; i < 20; i++ { // 2を累乗した値を返す(2 -> 4 -> 16 -> 32...) count := int(math.Pow(2, float64(i))) fmt.Fprintf( tw, "%d\t%v\t%v\n", count, test(count, &m, m.RLocker()), test(count, &m, &m), ) } } |
考察
出力結果
先に出力結果を見ておきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ go run main.go Readers RWMutex Mutex 1 36.663µs 2.545µs 2 5.372µs 26.287µs 4 45.943µs 5.865µs 8 14.964µs 17.094µs 16 56.7µs 12.552µs 32 83.782µs 74.39µs 64 127.298µs 56.397µs 128 50.047µs 53.275µs 256 101.018µs 59.218µs 512 124.749µs 131.723µs 1024 250.146µs 202.885µs 2048 496.244µs 383.269µs 4096 812.039µs 750.433µs 8192 1.625088ms 1.713126ms 16384 3.365656ms 3.045955ms 32768 6.771882ms 6.154006ms 65536 13.407168ms 12.92258ms 131072 26.664752ms 24.714778ms 262144 52.523476ms 48.979968ms 524288 107.856305ms 100.10891ms |
このプログラムは、RWMutexとMutexの処理スピードの差を表しています。
私のPCだとそれほど効果的な結果にならないですね。
しかし、本来、メモリのロック(クリティカルセクションの切り替え)にはある程度の時間的コストがかかるので、RWMutexを使う方が効率的なロックを行えることがあるようです。
sync.Lockerについて
sync.Lockerはinterfaceで、Mutex/RWMutex型を満たしています。
1 2 3 4 5 |
// A Locker represents an object that can be locked and unlocked. type Locker interface { Lock() Unlock() } |
Mutex、RWMutexのソースコードを追って見ると以下のようにLockerインターフェースを実装しているのがわかります。
1 2 3 4 5 6 7 8 9 10 11 |
func (m *Mutex) Unlock() { } func (m *Mutex) Lock() { } func (rw *RWMutex) Unlock() { } func (rw *RWMutex) Lock() { } |
これにより、Producer, observer, testのそれぞれのsync.LockerにMutex型、RWMutex型を渡すことができます。
producerとobserver
producerは生産者、observerは消費者です。
ここでは、producerを1ナノ秒間スリープさせてobserverより非活発にしているようです。これにより意図的に読込処理のプロセスを書込処理のプロセスより多く実行されるようにしています。
RWMutexは、以下の構造体になっています。
1 2 3 4 5 6 7 |
type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers readerWait int32 // number of departing readers } |
RWMutexは、Mutexをラップしています。
そのため、以下のようにLockを呼び出すとMutexのLockが呼ばれるっぽいです。
1 2 |
var m sync.RWMutex m.Lock() <<< MutexのLock |
一方、m.RLocker()は何者でしょうか。
1 2 3 4 5 6 7 8 9 |
// RLocker returns a Locker interface that implements // the Lock and Unlock methods by calling rw.RLock and rw.RUnlock. func (rw *RWMutex) RLocker() Locker { return (*rlocker)(rw) } type rlocker RWMutex func (r *rlocker) Lock() { (*RWMutex)(r).RLock() } func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() } |
RLockerはインターフェースのようです。
そして、このインターフェースを介して、LockとUnlockを呼び出すと、それぞれRLock、RUnlockが呼ばれるようですね。
呼び出し先でLock,とRLock、RUnlockとRUnlockとを意識しないで使えるので、並行処理とは別にこの実装パターンは覚えておいて損はなさそうですね。
テーブル出力
出力結果で表示したテーブルですが、これはtabwriterを使って出力しています。
色々なツールに使えそうなので、便利そうです。
1 2 3 4 5 6 7 8 9 |
tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0) defer tw.Flush() fmt.Fprintf( tw, "%d\t%v\t%v\n", count, test(count, &m, m.RLocker()), test(count, &m, &m), ) |
おわりに
並行処理は難しい!
気を抜くとすぐにバグになるし、新しい用語やらパターンやらがいっぱい出てくるしで大変です。
しかし、並行処理プログラミングが実装できるようになると様々なものに応用できるので、マスターしておきたいところです。
それでは、また!
最近のコメント