Go言語における並行処理の同期:すべてを一度に学ぶ
Wenhao Wang
Dev Intern · Leapcell

Go言語における並行処理の同期:すべてを一度に学ぶ
Go言語プログラミングシステムにおいて、軽量スレッドとしてのgoroutineは、低いリソース消費と低い切り替えコストという大きな利点により、並行処理の効率的な実装を強力にサポートします。しかし、これらの並行して実行されるgoroutineを効果的に制御する方法は、開発者が直面する必要がある重要な問題となっています。
Go言語におけるgoroutineの並行制御方法の分析
Go言語プログラミングシステムにおいて、軽量スレッドとしてのgoroutineは、低いリソース消費と低い切り替えコストという大きな利点により、並行処理の効率的な実装を強力にサポートします。しかし、これらの並行して実行されるgoroutineを効果的に制御する方法は、開発者が直面する必要がある重要な問題となっています。
並行制御に関して言えば、ロックメカニズムが最初に検討される手段であることがよくあります。Go言語では、相互排他ロックsync.Mutex
や読み書きロックsync.RWMutex
などの関連するロックメカニズムも提供されており、さらに、アトミック操作sync/atomic
も備えています。ただし、これらの操作は主に並行処理中のデータのセキュリティに焦点を当てており、goroutine自体を直接対象としているわけではないことを明確にしておく必要があります。
この記事では、goroutineの並行動作を制御するための一般的な方法を紹介することに焦点を当てます。Go言語では、sync.WaitGroup
、channel
、およびContext
という3つの最も一般的な方法があります。
I. sync.WaitGroup
sync.WaitGroup
は、Go言語において非常に実用的な同期プリミティブであり、その主な機能は、開発者がgoroutineのグループが実行を完了するのを待機するのを支援することです。通常、sync.WaitGroup
は、次のシナリオで使用されます。
- メイン関数で、プログラムを終了する前に、goroutineのグループがすべて実行されていることを確認する必要がある場合。
- 関数内で複数のgoroutineが開始され、これらのgoroutineがすべて完了するまで結果を返す必要がないシナリオ。
- 関数が複数のgoroutineを開始し、それらがすべて完了した後に特定の操作を実行する必要がある場合。
- 関数が複数のgoroutineを開始し、それらがすべて完了した後に特定のリソースを閉じる必要がある状況。
- 関数が複数のgoroutineを開始し、それらがすべて完了するまでループを終了できない場合。
sync.WaitGroup
を使用する場合、特定の手順は次のとおりです。まず、sync.WaitGroup
オブジェクトを作成します。次に、このオブジェクトのAdd
メソッドを使用して、待機するgoroutineの数を指定します。その後、go
キーワードを使用して複数のgoroutineを開始し、各goroutine内でsync.WaitGroup
オブジェクトのDone
メソッドを呼び出して、goroutineが実行を完了したことを示します。最後に、sync.WaitGroup
オブジェクトのWait
メソッドを呼び出して、すべてのgoroutineが完了するのを待ちます。
以下は簡単な例です。この例では、それぞれ0秒、1秒、2秒スリープする3つのgoroutineを開始し、メイン関数はこれら3つのgoroutineが終了した後に終了します。
package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(i int) { defer wg.Done() fmt.Printf("sub goroutine sleep: %ds\n", i) time.Sleep(time.Duration(i) * time.Second) }(i) } wg.Wait() fmt.Println("main func done") }
II. channel
Go言語では、channel
は、開発者がgoroutineの並行処理をより適切に制御するのに役立つ強力なツールです。以下は、channel
を使用してgoroutineの並行処理を制御するためのいくつかの一般的な方法です。
(I) バッファなしチャネルを同期に使用する
バッファなしchannel
を使用すると、プロデューサーとコンシューマーのパターンを実装できます。このパターンでは、1つのgoroutineがデータの生成を担当し、別のgoroutineがデータの消費を担当します。プロデューサーgoroutineがchannel
にデータを送信すると、コンシューマーgoroutineはブロック状態になり、データが到着するのを待ちます。このようにして、プロデューサーとコンシューマー間のデータの同期を保証できます。
以下は簡単なサンプルコードです。
package main import ( "fmt" "sync" "time" ) func producer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < 10; i++ { ch <- i fmt.Println("produced", i) time.Sleep(100 * time.Millisecond) } close(ch) } func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() for i := range ch { fmt.Println("consumed", i) time.Sleep(150 * time.Millisecond) } } func main() { var wg sync.WaitGroup ch := make(chan int) wg.Add(2) go producer(ch, &wg) go consumer(ch, &wg) wg.Wait() }
この例では、バッファなしchannel
を作成して、プロデューサーgoroutineとコンシューマーgoroutineの間でデータを転送します。プロデューサーgoroutineはchannel
にデータを送信し、コンシューマーgoroutineはchannel
からデータを受信します。プロデューサーgoroutineでは、time.Sleep
関数を使用して、データの生成に必要な時間をシミュレートします。コンシューマーgoroutineでは、time.Sleep
関数も使用して、データの消費に必要な時間をシミュレートします。最後に、sync.WaitGroup
を使用して、すべてのgoroutineが完了するのを待ちます。
(II) バッファ付きチャネルをレート制限に使用する
バッファ付きchannel
を使用すると、並行goroutineの数を制限できます。具体的なアプローチは、channel
の容量を目的の並行goroutineの最大数に設定することです。各goroutineを開始する前に、値をchannel
に送信します。goroutineの実行が終了したら、channel
から値を受信します。このようにして、同時に実行されるgoroutineの数が指定された最大並行数を超えないようにすることができます。
以下は簡単なサンプルコードです。
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup maxConcurrency := 3 semaphore := make(chan struct{}, maxConcurrency) for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() semaphore <- struct{}{} fmt.Println("goroutine", i, "started") // do some work fmt.Println("goroutine", i, "finished") <-semaphore }() } wg.Wait() }
この例では、バッファ付きchannel
を作成し、そのバッファサイズは3です。次に、10個のgoroutineを開始します。各goroutineでは、空のstructをchannel
に送信して、goroutineが実行を開始したことを示します。goroutineが完了したら、空のstructをchannel
から受信して、goroutineが実行を完了したことを示します。このようにして、同時に実行されるgoroutineの数が3を超えないようにすることができます。
III. Context
Go言語では、Context
はgoroutineの並行処理を制御するための重要な手段です。以下は、Context
を使用してgoroutineの並行処理を制御するためのいくつかの一般的な方法です。
(I) タイムアウト制御
場合によっては、プログラムの長期的なブロックやデッドロックなどの問題を回避するために、goroutineの実行時間を制限する必要があります。Context
は、開発者がgoroutineの実行時間をより適切に制御するのに役立ちます。具体的な操作は、タイムアウト期間を持つContext
を作成し、それをgoroutineに渡すことです。goroutineがタイムアウト期間内に実行を完了できない場合、Context
のDone
メソッドを使用してgoroutineの実行をキャンセルできます。
以下は簡単なサンプルコードです。
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go func() { for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } }() time.Sleep(3 * time.Second) }
この例では、タイムアウト期間を持つContext
を作成し、それをgoroutineに渡します。goroutine内では、select
ステートメントを使用して、Context
のDone
メソッドをリッスンします。Context
がタイムアウトすると、goroutineの実行はキャンセルされます。
(II) キャンセル操作
プログラムの実行中に、特定のgoroutineの実行をキャンセルする必要がある場合があります。Context
は、開発者がgoroutineのキャンセル操作をより適切に制御するのに役立ちます。具体的なアプローチは、キャンセル関数を持つContext
を作成し、それをgoroutineに渡すことです。goroutineの実行をキャンセルする必要がある場合は、Context
のCancel
メソッドを呼び出すことができます。
以下は簡単なサンプルコードです。
package main import ( "context" "fmt" "sync" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } }() time.Sleep(2 * time.Second) cancel() wg.Wait() }
この例では、キャンセル関数を持つContext
を作成し、それをgoroutineに渡します。goroutine内では、select
ステートメントを使用して、Context
のDone
メソッドをリッスンします。Context
がキャンセルされると、goroutineの実行はキャンセルされます。メイン関数では、time.Sleep
関数を使用して、プログラムの実行中にgoroutineの実行をキャンセルする必要がある特定の瞬間をシミュレートし、次にContext
のCancel
メソッドを呼び出します。
(III) リソース管理
一部のシナリオでは、リソースリークや競合状態などの問題を回避するために、goroutineが使用するリソースを管理する必要があります。Context
は、開発者がgoroutineが使用するリソースをより適切に管理するのに役立ちます。具体的な操作は、リソースをContext
に関連付け、Context
をgoroutineに渡すことです。goroutineが実行を完了すると、Context
を使用してリソースを解放したり、他のリソース管理操作を実行したりできます。
以下は簡単なサンプルコードです。
package main import ( "context" "fmt" "sync" "time" ) func worker(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("goroutine finished") return default: fmt.Println("goroutine running") time.Sleep(500 * time.Millisecond) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go worker(ctx, &wg) time.Sleep(2 * time.Second) cancel() wg.Wait() }
この例では、キャンセル関数を持つContext
を作成し、それをgoroutineに渡します。goroutine内では、select
ステートメントを使用して、Context
のDone
メソッドをリッスンします。Context
がキャンセルされると、goroutineの実行はキャンセルされます。メイン関数では、time.Sleep
関数を使用して、プログラムの実行中にgoroutineの実行をキャンセルする必要がある特定の瞬間をシミュレートし、次にContext
のCancel
メソッドを呼び出します。
Leapcell:Golangアプリホスティングのための次世代サーバーレスプラットフォーム
最後に、Goサービスのデプロイに最適なプラットフォームをお勧めします。Leapcell
1. 複数言語のサポート
- JavaScript、Python、Go、またはRustで開発します。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に応じてのみ支払い - リクエストも料金もありません。
3. 比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60msで694万リクエストをサポートします。
4. 合理化された開発者エクスペリエンス
- 楽なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムメトリックとロギング。
5. 簡単なスケーラビリティと高パフォーマンス
- 簡単な自動スケーリングにより、高い並行処理に対応できます。
- 運用上のオーバーヘッドはゼロ - 構築に集中するだけです。
Leapcell Twitter: https://x.com/LeapcellHQ