Goにおけるsync vs. channelの使用時期
Emily Parker
Product Engineer · Leapcell

sync
と channel
のどちらを選ぶべきか
C言語でプログラミングする場合、通常は通信に共有メモリを使用します。複数のスレッドが共有データに対して同時に操作を行う場合、データの安全性を確保し、スレッドの同期を制御するために、必要に応じてミューテックスを使用してロックおよびアンロックを行います。
ただし、Goでは、通信によるメモリの共有が推奨されています。つまり、チャネルを使用してクリティカルセクションの同期メカニズムを完了します。
とはいえ、 Goのチャネルは比較的高レベルなプリミティブであり、当然ながらsync
パッケージのロック機構と比較してパフォーマンスが劣ります。興味があれば、自分で簡単なベンチマークテストを作成してパフォーマンスを比較し、結果をコメントで議論してください。
さらに、sync
パッケージを使用して同期を制御する場合、構造体オブジェクトのオーナーシップを失うことはなく、複数のgoroutineがクリティカルセクションリソースへのアクセスを同期させることを許可できます。したがって、要件がこのシナリオに適合する場合は、より合理的かつ効率的であるため、sync
パッケージを同期に使用することをお勧めします。
sync
パッケージを同期に選択すべき理由:
- 複数のgoroutineがクリティカルセクションリソースに安全にアクセスできるようにしながら、構造体の制御を失いたくない場合。
- より高いパフォーマンスが必要な場合。
sync
の Mutex と RWMutex
sync
パッケージのソースコードを見ると、次の構造が含まれていることがわかります。
- Mutex
- RWMutex
- Once
- Cond
- Pool
atomic
パッケージでのアトミック操作
これらの中で、Mutex
が最も一般的に使用されます。特にチャネルの使用にまだ慣れていない場合は、Mutexが非常に役立つことがわかります。対照的に、RWMutex
の使用頻度は低くなります。
Mutex
と RWMutex
を使用した場合のパフォーマンスの違いに注意したことはありますか? ほとんどの人はデフォルトでmutexを使用するので、簡単なデモを作成してそのパフォーマンスを比較してみましょう。
var ( mu sync.Mutex murw sync.RWMutex tt1 = 1 tt2 = 2 tt3 = 3 ) // Mutexを使用してデータを読み取る func BenchmarkReadMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { mu.Lock() _ = tt1 mu.Unlock() } }) } // RWMutexを使用してデータを読み取る func BenchmarkReadRWMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { murw.RLock() _ = tt2 murw.RUnlock() } }) } // RWMutexを使用してデータの読み取りと書き込みを行う func BenchmarkWriteRWMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { murw.Lock() tt3++ murw.Unlock() } }) }
3つの簡単なベンチマークテストを作成しました。
- ミューテックスロックを使用してデータを読み取ります。
- リード/ライトロックの読み取りロックを使用してデータを読み取ります。
- リード/ライトロックを使用してデータを読み書きします。
$ go test -bench . bbb_test.go --cpu 2 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-2 39638757 30.45 ns/op BenchmarkReadRWMutex-2 43082371 26.97 ns/op BenchmarkWriteRWMutex-2 16383997 71.35 ns/op $ go test -bench . bbb_test.go --cpu 4 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-4 17066666 73.47 ns/op BenchmarkReadRWMutex-4 43885633 30.33 ns/op BenchmarkWriteRWMutex-4 10593098 110.3 ns/op $ go test -bench . bbb_test.go --cpu 8 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-8 8969340 129.0 ns/op BenchmarkReadRWMutex-8 36451077 33.46 ns/op BenchmarkWriteRWMutex-8 7728303 158.5 ns/op $ go test -bench . bbb_test.go --cpu 16 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-16 8533333 132.6 ns/op BenchmarkReadRWMutex-16 39638757 29.98 ns/op BenchmarkWriteRWMutex-16 6751646 173.9 ns/op $ go test -bench . bbb_test.go --cpu 128 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-128 10155368 116.0 ns/op BenchmarkReadRWMutex-128 35108558 33.27 ns/op BenchmarkWriteRWMutex-128 6334021 195.3 ns/op
結果からわかるように、並行性が低い場合、mutexロックと(RWMutexからの)読み取りロックのパフォーマンスは似ています。 並行性が高くなるにつれて、RWMutexの読み取りロックのパフォーマンスは大幅に変化しませんが、mutexとRWMutexの両方のパフォーマンスは並行性が高くなるにつれて低下します。
RWMutexは、読み取りが多く、書き込みが少ないシナリオに適していることは明らかです。 多くの同時読み取り操作があるシナリオでは、複数のgoroutineが読み取りロックを同時に取得できるため、ロックの競合と待ち時間が短縮されます。
ただし、通常のmutexを使用すると、並行性がある場合、一度に1つのgoroutineのみがロックを取得できます。 他のgoroutineはブロックされ、待機する必要があるため、パフォーマンスに悪影響を与えます。
たとえば、実際に通常のmutexを使用すると、どのような問題が発生する可能性があるかを見てみましょう。
sync
を使用する際の注意点
sync
パッケージのロックを使用する場合、MutexまたはRWMutexが既に使用されている場合はコピーしないでください。
簡単なデモを次に示します。
var mu sync.Mutex // MutexまたはRWMutexを既に使用している場合はコピーしないでください。 // コピーする必要がある場合は、使用前にのみ行ってください。 func main() { go func(mm sync.Mutex) { for { mm.Lock() time.Sleep(time.Second * 1) fmt.Println("g2") mm.Unlock() } }(mu) mu.Lock() go func(mm sync.Mutex) { for { mm.Lock() time.Sleep(time.Second * 1) fmt.Println("g3") mm.Unlock() } }(mu) time.Sleep(time.Second * 1) fmt.Println("g1") mu.Unlock() time.Sleep(time.Second * 20) }
このコードを実行すると、"g3"
が出力されないことに気付くでしょう。 これは、"g3"
を含むgoroutineがデッドロックし、アンロックを呼び出す機会がないことを意味します。
この理由Mutexの内部構造にあります。 見てみましょう。
//... // Mutexは、最初に使用した後はコピーしてはなりません。 //... type Mutex struct { state int32 sema uint32 }
Mutex構造体には、内部のstate
(mutexの状態を表します)とsema
(mutexのセマフォを制御するために使用されます)が含まれています。 Mutexが初期化されると、両方とも0になります。 ただし、Mutexをロックすると、その状態がロック済みに変わります。 この時点で、別のgoroutineがこのMutexをコピーし、独自のコンテキストでロックすると、デッドロックが発生します。 これは覚えておくべき重要な詳細です。
複数のgoroutineがMutexを使用する必要があるシナリオがある場合は、クロージャを使用するか、ロックをラップする構造体へのポインタまたはアドレスを渡すことができます。 これにより、ロックを使用する際に予期しない結果を回避できます。
sync.Once
sync
パッケージの他のメンバーをどれくらいの頻度で使用しますか? より頻繁に使用されるものの1つはsync.Once
です。 sync.Once
の使用方法と注意する必要があることを見てみましょう。
CまたはC++では、シングルトン(プログラムのライフサイクル中にインスタンスが1つしかない)が必要な場合、シングルトンパターンを使用することがよくあります。 ここで、sync.Once
はGoでシングルトンを実装するのに最適です。
sync.Once
は、特定の関数がプログラムのライフサイクル中に一度だけ実行されることを保証します。 これは、パッケージごとに1回呼び出される init
関数よりも柔軟性があります。
注意すべき点の1つ: sync.Once
内で実行される関数がパニックを起こした場合でも、実行されたと見なされます。 その後、再度 sync.Once
に入ろうとするロジックは関数を実行しません。
通常、sync.Once
は、オブジェクト/リソースの初期化およびクリーンアップに使用され、繰り返しの操作を防ぎます。 デモを次に示します。
- main関数は3つのgoroutineを開始し、
sync.WaitGroup
を使用して子goroutineの終了を管理および待機します。 - すべてのgoroutineを開始した後、main関数は2秒間待機し、インスタンスを作成してインスタンスを取得しようとします。
- 各goroutineもインスタンスを取得しようとします。
- 1つのgoroutineがOnceに入り、ロジックを実行するとすぐに、パニックが発生します。
- パニックが発生したgoroutineは例外をキャッチします。 この時点で、グローバルインスタンスはすでに初期化されており、他のgoroutineは依然としてOnce内の関数に入ることができません。
type Instance struct { Name string } var instance *Instance var on sync.Once func GetInstance(num int) *Instance { defer func() { if err := recover(); err != nil { fmt.Printf("num %d ,get instance and catch error ... \n", num) } }() on.Do(func() { instance = &Instance{Name: "Leapcell"} fmt.Printf("%d enter once ... \n", num) panic("panic....") }) return instance } func main() { var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(i int) { ins := GetInstance(i) fmt.Printf("%d: ins:%+v , p=%p\n", i, ins, ins) wg.Done() }(i) } time.Sleep(time.Second * 2) ins := GetInstance(9) fmt.Printf("9: ins:%+v , p=%p\n", ins, ins) wg.Wait() }
出力から、goroutine 0がOnceに入り、パニックが発生したため、そのgoroutineのGetInstanceによって返される結果はnilであることがわかります。
メインを含む他のすべてのgoroutineは、予想どおりinstance
のアドレスを取得でき、アドレスは同じです。 これは、初期化がグローバルに1回だけ発生することを示しています。
$ go run main.go 0 enter once ... num %d ,get instance and catch error ... 0 0: ins:<nil> , p=0x0 1: ins:&{Name:Leapcell} , p=0xc000086000 2: ins:&{Name:Leapcell} , p=0xc000086000 9: ins:&{Name:Leapcell} , p=0xc000086000
Leapcellは、Goプロジェクトをホストするための最適な選択肢です。
Leapcell は、Webホスティング、非同期タスク、およびRedis用の次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rust で開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い — リクエストも料金もありません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例: $25 で平均応答時間 60 ミリ秒で 694 万件のリクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的な UI。
- 完全に自動化された CI/CD パイプラインと GitOps 統合。
- 実用的な洞察のためのリアルタイムのメトリクスとロギング。
簡単なスケーラビリティと高性能
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ — 構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください: @LeapcellHQ