0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoにおけるRwMutexとMutexのパフォーマンスを複数のシナリオで比較

Posted at

Group261.png

Leapcell: The Next-Gen Serverless Platform for Golang app Hosting

Golang ロックの性能に関する研究と分析

ソフトウェア開発の分野において、Golang のロックの性能をテストすることは実践的なタスクです。最近、友人が一つの質問を投げかけました:スライスに対してスレッドセーフな読み書き操作を行う場合、読み書きロック(rwlock)とミューテックスロック(mutex)のどちらを選ぶべきか、また、どちらのロックが性能が良いのか?この質問が深い議論を引き起こしました。

Ⅰ. ロック性能テストの背景と目的

マルチスレッドプログラミングのシナリオにおいて、データのスレッドセーフを確保することは非常に重要です。スライスなどのデータ構造に対する読み書き操作において、適切なロック機構を選択することは、プログラムの性能に大きく影響を与えることができます。この研究の目的は、異なるシナリオにおける読み書きロックとミューテックスロックの性能を比較することにより、開発者が実際のアプリケーションにおいてロック機構を選択する際の参考となるようにすることです。

Ⅱ. 異なるシナリオにおける異なるロック機構の性能分析

(Ⅰ) 読み書きロック(Rwmutex)とミューテックスロック(Mutex)の性能比較に関する理論的議論

どのようなシナリオで読み書きロックがミューテックスロックよりも性能が良いのかは、深く分析する価値のある問題です。ロックのロック(lock)とアンロック(unlock)のプロセスにおいて、入出力(io)ロジックや複雑な計算ロジックがない場合、理論的には、ミューテックスロックの方が読み書きロックよりも効率的かもしれません。現在、コミュニティには様々な読み書きロックの設計と実装方法があり、その多くは二つのロックと読み取りカウンタを抽象化することで実現されています。

(Ⅱ) C++ 環境におけるロックの性能比較の参考

以前、C++ 環境におけるミューテックスロック(lock)と読み書きロック(rwlock)の性能比較が行われています。単純な代入ロジックのシナリオにおいて、ベンチマークテストの結果は期待どおりで、すなわち、ミューテックスロックの性能が読み書きロックよりも良いです。中間ロジックが空の io 読み書き操作の場合、読み書きロックの性能がミューテックスロックよりも高く、これも一般的な知識と一致しています。中間ロジックが map 検索の場合、読み書きロックもミューテックスロックよりも高い性能を示します。これは、map が複雑なデータ構造であり、キーを検索する際には、ハッシュコードを計算し、ハッシュコードを使って配列内の対応するバケットを探し、その後、連結リストから関連するキーを探す必要があるためです。具体的な性能データは以下の通りです:

  • 単純な代入:
    • raw_lock は 1.732199s かかります;
    • raw_rwlock は 3.420338s かかります
  • io 操作:
    • simple_lock は 13.858138s かかります;
    • simple_rwlock は 8.94691s かかります
  • map:
    • lock は 2.729701s かかります;
    • rwlock は 0.300296s かかります

(Ⅲ) Golang 環境における sync.rwmutex と sync.mutex の性能テスト

Golang 環境における読み書きロックとミューテックスロックの性能を深く探るために、以下のテストを行いました。テストコードは以下の通りです:

package main


import (
    "fmt"
    "sync"
    "time"
)

var (
    num  = 1000 * 10
    gnum = 1000
)

func main() {
    fmt.Println("only read")
    testRwmutexReadOnly()
    testMutexReadOnly()

    fmt.Println("write and read")
    testRwmutexWriteRead()
    testMutexWriteRead()

    fmt.Println("write only")
    testRwmutexWriteOnly()
    testMutexWriteOnly()
}

func testRwmutexReadOnly() {
    var w = &sync.WaitGroup{}
    var rwmutexTmp = newRwmutex()
    w.Add(gnum)
    t1 := time.Now()
    for i := 0; i < gnum; i++ {
        go func() {
            defer w.Done()
            for in := 0; in < num; in++ {
                rwmutexTmp.get(in)
            }
        }()
    }
    w.Wait()
    fmt.Println("testRwmutexReadOnly cost:", time.Now().Sub(t1).String())
}

func testRwmutexWriteOnly() {
    var w = &sync.WaitGroup{}
    var rwmutexTmp = newRwmutex()
    w.Add(gnum)
    t1 := time.Now()
    for i := 0; i < gnum; i++ {
        go func() {
            defer w.Done()
            for in := 0; in < num; in++ {
                rwmutexTmp.set(in, in)
            }
        }()
    }
    w.Wait()
    fmt.Println("testRwmutexWriteOnly cost:", time.Now().Sub(t1).String())
}

func testRwmutexWriteRead() {
    var w = &sync.WaitGroup{}
    var rwmutexTmp = newRwmutex()
    w.Add(gnum)
    t1 := time.Now()
    for i := 0; i < gnum; i++ {
        if i%2 == 0 {
            go func() {
                defer w.Done()
                for in := 0; in < num; in++ {
                    rwmutexTmp.get(in)
                }
            }()
        } else {
            go func() {
                defer w.Done()
                for in := 0; in < num; in++ {
                    rwmutexTmp.set(in, in)
                }
            }()
        }
    }
    w.Wait()
    fmt.Println("testRwmutexWriteRead cost:", time.Now().Sub(t1).String())
}

func testMutexReadOnly() {
    var w = &sync.WaitGroup{}
    var mutexTmp = newMutex()
    w.Add(gnum)

    t1 := time.Now()
    for i := 0; i < gnum; i++ {
        go func() {
            defer w.Done()
            for in := 0; in < num; in++ {
                mutexTmp.get(in)
            }
        }()
    }
    w.Wait()
    fmt.Println("testMutexReadOnly cost:", time.Now().Sub(t1).String())
}

func testMutexWriteOnly() {
    var w = &sync.WaitGroup{}
    var mutexTmp = newMutex()
    w.Add(gnum)

    t1 := time.Now()
    for i := 0; i < gnum; i++ {
        go func() {
            defer w.Done()
            for in := 0; in < num; in++ {
                mutexTmp.set(in, in)
            }
        }()
    }
    w.Wait()
    fmt.Println("testMutexWriteOnly cost:", time.Now().Sub(t1).String())
}

func testMutexWriteRead() {
    var w = &sync.WaitGroup{}
    var mutexTmp = newMutex()
    w.Add(gnum)
    t1 := time.Now()
    for i := 0; i < gnum; i++ {
        if i%2 == 0 {
            go func() {
                defer w.Done()
                for in := 0; in < num; in++ {
                    mutexTmp.get(in)
                }
            }()
        } else {
            go func() {
                defer w.Done()
                for in := 0; in < num; in++ {
                    mutexTmp.set(in, in)
                }
            }()
        }

    }
    w.Wait()
    fmt.Println("testMutexWriteRead cost:", time.Now().Sub(t1).String())
}

func newRwmutex() *rwmutex {
    var t = &rwmutex{}
    t.mu = &sync.RWMutex{}
    t.ipmap = make(map[int]int, 100)

    for i := 0; i < 100; i++ {
        t.ipmap[i] = 0
    }
    return t
}

type rwmutex struct {
    mu    *sync.RWMutex
    ipmap map[int]int
}

func (t *rwmutex) get(i int) int {
    t.mu.RLock()
    defer t.mu.RUnlock()

    return t.ipmap[i]
}

func (t *rwmutex) set(k, v int) {
    t.mu.Lock()
    defer t.mu.Unlock()

    k = k % 100
    t.ipmap[k] = v
}

func newMutex() *mutex {
    var t = &mutex{}
    t.mu = &sync.Mutex{}
    t.ipmap = make(map[int]int, 100)

    for i := 0; i < 100; i++ {
        t.ipmap[i] = 0
    }
    return t
}

type mutex struct {
    mu    *sync.Mutex
    ipmap map[int]int
}

func (t *mutex) get(i int) int {
    t.mu.Lock()
    defer t.mu.Unlock()

    return t.ipmap[i]
}

func (t *mutex) set(k, v int) {
    t.mu.Lock()
    defer t.mu.Unlock()

    k = k % 100
    t.ipmap[k] = v
}

テスト結果は以下の通りです:
複数の goroutine で mutex と rwmutex を使用するシナリオにおいて、それぞれ読み取りのみ、書き込みのみ、読み書きの 3 つのテストシナリオをテストしました。結果は、書き込みのみのシナリオでのみ、mutex の性能が rwmutex よりもわずかに高いように見えます。

  • 読み取りのみ:
    • testRwmutexReadOnly cost: 455.566965ms
    • testMutexReadOnly cost: 2.13687988s
  • 読み書き:
    • testRwmutexWriteRead cost: 1.79215194s
    • testMutexWriteRead cost: 2.62997403s
  • 書き込みのみ:
    • testRwmutexWriteOnly cost: 2.6378979159s
    • testMutexWriteOnly cost: 2.39077869s

さらに、map の読み書きロジックをカウンタのグローバルなインクリメントとデクリメントに置き換えた場合、テスト結果は上記の状況と似ており、すなわち、書き込みのみのシナリオで、mutex の性能が rwlock よりもわずかに高いです。

  • 読み取りのみ:
    • testRwmutexReadOnly cost: 10.483448ms
    • testMutexReadOnly cost: 10.808006ms
  • 読み書き:
    • testRwmutexWriteRead cost: 12.405655ms
    • testMutexWriteRead cost: 14.571228ms
  • 書き込みのみ:
    • testRwmutexWriteOnly cost: 13.453028ms
    • testMutexWriteOnly cost: 13.782282ms

Ⅲ. Golang の sync.RwMutex のソースコード分析

Golang の sync.RwMutex の構造は、読み取りロック、書き込みロック、および読み取りカウンタを含んでいます。コミュニティにおける一般的な実装方法と最大の違いは、読み取りカウンタの操作にアトミック命令(atomic)を使用していることです。具体的な構造定義は以下の通りです:

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
}

(Ⅰ) 読み取りロックの取得プロセス

読み取りロックの取得は、直接アトミックで減算操作を行います。readerCount が 0 未満の場合、書き込み操作が待機していることを示し、このとき、読み取りロックを待つ必要があります。コード実装は以下の通りです:

func (rw *RWMutex) RLock() {
    if race.Enabled {
        _ = rw.w.state
        race.Disable()
    }
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // A writer is pending, wait for it.
        runtime_Semacquire(&rw.readerSem)
    }
    if race.Enabled {
        race.Enable()
        race.Acquire(unsafe.Pointer(&rw.readerSem))
    }
}

(Ⅱ) 読み取りロックの解放プロセス

読み取りロックの解放も、カウントに対してアトミックで操作を行います。読み取りがない場合、書き込みロックを解放します。関連するコードは以下の通りです:

func (rw *RWMutex) RUnlock() {
    if race.Enabled {
        _ = rw.w.state
        race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
        race.Disable()
    }
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        if r+1 == 0 || r+1 == -rwmutex
            race.Enable()
            throw("sync: RUnlock of unlocked RWMutex")
        }
        // A writer is pending.
        if atomic.AddInt32(&rw.readerWait, -1) == 0 {
            // The last reader unblocks the writer.
            runtime_Semrelease(&rw.writerSem, false)
        }
    }
    if race.Enabled {
        race.Enable()
    }
}

(Ⅲ) 書き込みロックの取得と解放のプロセス

書き込みロックの取得プロセスでは、まず、読み取り操作があるかどうかを判断します。読み取り操作がある場合は、読み取り操作が完了するまで待機し、目覚めるまで待ちます。書き込みロックを解放するときは、同時に読み取りロックも解放され、その後、読み取りロックを待っている goroutine が目覚めます。関連するコードは以下の通りです:

func (rw *RWMutex) Lock() {
    if race.Enabled {
        _ = rw.w.state
        race.Disable()
    }
    // First, resolve competition with other writers.
    rw.w.Lock()
    // Announce to readers there is a pending writer.
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    // Wait for active readers.
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_Semacquire(&rw.writerSem)
    }
    if race.Enabled {
        race.Enable()
        race.Acquire(unsafe.Pointer(&rw.readerSem))
        race.Acquire(unsafe.Pointer(&rw.writerSem))
    }
}

func (rw *RWMutex) Unlock() {
    if race.Enabled {
        _ = rw.w.state
        race.Release(unsafe.Pointer(&rw.readerSem))
        race.Release(unsafe.Pointer(&rw.writerSem))
        race.Disable()
    }

    // Announce to readers there is no active writer.
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        race.Enable()
        throw("sync: Unlock of unlocked RWMutex")
    }
    // Unblock blocked readers, if any.
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false)
    }
    // Allow other writers to proceed.
    rw.w.Unlock()
    if race.Enabled {
        race.Enable()
    }
}

Ⅳ. まとめと提案

ロック競合の問題は、常に高い並列性を持つシステムが直面する重要な課題の一つです。上記のように、mutex と map を組み合わせて使用するシナリオにおいて、Go の 1.9 以降のバージョンでは、sync.Map を代わりに使用することを検討することができます。読み取り操作が頻繁で書き込み操作が少ないシナリオでは、sync.Map の性能は sync.RwMutex と map の組み合わせに比べて大きな利点があります。

sync.Map の実装原理を深く研究した後、その書き込み操作の性能が比較的低いことがわかります。読み取り操作は、書き換え時のコピー(copy on write)方法を通じてロックフリーの読み取りを実現することができますが、書き込み操作は依然としてロック機構を伴います。ロック競合の圧力を軽減するために、Java の ConcurrentMap と同じようなセグメントロックの方法を参考にすることができます。

セグメントロックに加えて、アトミック比較と交換(atomic cas)命令を使用して、楽観的ロックを実装することもでき、ロック競合の問題を効果的に解決し、高い並列性のシナリオにおけるシステムの性能を向上させることができます。

Leapcell: The Next-Gen Serverless Platform for Golang app Hosting

最後に、Golang サービスをデプロイするのに最適なプラットフォームをお勧めします:Leapcell

barndpic.png

1. 多言語対応

  • JavaScript、Python、Go、または Rust で開発できます。

2. 無料で無制限のプロジェクトをデプロイ

  • 使用分のみ課金 — リクエストがなければ、請求はありません。

3. 抜群のコスト効率

  • 使用量に応じて請求され、アイドル状態での請求はありません。
  • 例: 平均応答時間 60ms で 694 万件のリクエストに対応するのに 25 ドル。

4. 合理化された開発者体験

  • 直感的な UI で簡単にセットアップできます。
  • 完全自動化された CI/CD パイプラインと GitOps 統合。
  • アクション可能なインサイトを得るためのリアルタイムメトリクスとロギング。

5. 簡単なスケーラビリティと高性能

  • 高い並列性を簡単に処理するための自動スケーリング。
  • オペレーションオーバーヘッドはゼロ — 構築に集中するだけです。

Frame3-withpadding2x.png

詳細はドキュメントをご覧ください!

Leapcell Twitter: https://x.com/LeapcellHQ

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?