kawasin73のブログ

技術記事とかいろんなことをかくブログです

Go : Mutex は channel の 4 倍速い

速ければ速いほど良い。どうもかわしんです。トランザクションを実装中です。

トランザクションの並行処理で S2PL (Strict Two Phase Lock) を Go で実装しようとしているのですが、どうしても昇格可能な Reader Writer Mutex が必要になり、Github にいい実装がなかったので自分で実装しようとしています。

さて、独自の Mutex を実装するにあたり goroutine 同士の待ち合わせは何かを使って実現する必要がありますが、Go には sync.Mutex とチャネルがあります。

どちらとも、ロックしてアンロックするということができます。振る舞いは同じです。

振る舞いが同じとなればどちらが速いかが重要となります。

ということで実験してみました。

実験

環境は以下の通りです。

ベンチマークコード

以下を適当に 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 では必ずチャネルを使わないといけなくなりそうなのが残念です。