速ければ速いほど良い。どうもかわしんです。トランザクションを実装中です。
トランザクションの並行処理で S2PL (Strict Two Phase Lock) を Go で実装しようとしているのですが、どうしても昇格可能な Reader Writer Mutex が必要になり、Github にいい実装がなかったので自分で実装しようとしています。
さて、独自の Mutex を実装するにあたり goroutine 同士の待ち合わせは何かを使って実現する必要がありますが、Go には sync.Mutex
とチャネルがあります。
どちらとも、ロックしてアンロックするということができます。振る舞いは同じです。
振る舞いが同じとなればどちらが速いかが重要となります。
ということで実験してみました。
実験
環境は以下の通りです。
- OS : macOS 10.14.6
- Hardware : Macbook Pro 13-inch 2018
- CPU : 2.7 GHz Intel Core i7, 4 core
- Go : 1.13.4
ベンチマークコード
以下を適当に main_test.go
としておきます。
package main import ( "sync" "testing" ) func BenchmarkMutex(b *testing.B) { var mu sync.Mutex var n int for i := 0; i < b.N; i++ { mu.Lock() n++ mu.Unlock() } } func BenchmarkChan(b *testing.B) { ch := make(chan struct{}, 1) var n int for i := 0; i < b.N; i++ { ch <- struct{}{} n++ <-ch } } func BenchmarkMultiMutex(b *testing.B) { var mu sync.Mutex var n int var wg sync.WaitGroup wg.Add(10) for j := 0; j < 10; j++ { go func() { for i := 0; i < b.N; i++ { mu.Lock() n++ mu.Unlock() } wg.Done() }() } wg.Wait() } func BenchmarkMultiChan(b *testing.B) { ch := make(chan struct{}, 1) var n int var wg sync.WaitGroup wg.Add(10) for j := 0; j < 10; j++ { go func() { for i := 0; i < b.N; i++ { ch <- struct{}{} n++ <-ch } wg.Done() }() } wg.Wait() }
BenchmarkMulti****()
は、10 の goroutine で同時に動かすものです。
結果
GOMAXPROCS=1
でおこなったものがこれです。
$ GOMAXPROCS=1 GO111MODULE=off go test -bench . goos: darwin goarch: amd64 BenchmarkMutex 100000000 10.4 ns/op BenchmarkChan 28564992 40.5 ns/op BenchmarkMultiMutex 9439315 126 ns/op BenchmarkMultiChan 1000000 1189 ns/op PASS ok _/Users/kawasin73/work/sample-go/rwlock 4.808s
普通のマルチスレッドでおこなったものがこれです。
$ GO111MODULE=off go test -bench . goos: darwin goarch: amd64 BenchmarkMutex-8 100000000 10.7 ns/op BenchmarkChan-8 27600146 42.3 ns/op BenchmarkMultiMutex-8 2536651 494 ns/op BenchmarkMultiChan-8 736453 1710 ns/op PASS ok _/Users/kawasin73/work/sample-go/rwlock 5.307s
まとめ
sync.Mutex
の方がチャネルよりだいたい 4 倍速いです。チャネルは内部でロックを取った上でごにょごにょやっているので遅いのは当たり前です。
また、シングルスレッドにするとスレッド同士の待ち合わせが無くなったりシングルスレッド用の最適化があるのかどうかはわかりませんが、速くなります。速くなるというよりは、マルチスレッドで動かすと遅くなるという表現の方が理解しやすそうですが。
シンプルな昇格可能な RWMutex
を作る分には sync.Mutex
を使う方向でできそうです。
ただ、タイムアウトをさせようとすると Go では必ずチャネルを使わないといけなくなりそうなのが残念です。