Contents
Go言語 並行処理 入門:goroutineとchannelによる基本概念を理解する
Go 言語で並行処理を実装する際、goroutineやchannelといったキーワードが登場します。これらは複数のタスクを効率的に処理するための核となる技術です。本記事では、初心者向けに理論と実践をバランスよく解説し、実際に動かせるコード例を通じて並行処理の仕組みを理解していただきます。
Goで並行処理を実現するための基本概念
Go 言語ではgoroutineを使って複数のタスクを同時に進めることが可能です。これは軽量なスレッドとして動作し、リソース消費が少ないのが特徴です。また、channelを用いることでgoroutine間でデータを安全にやり取りできます。さらに複数のgoroutineが終了するのを待つには、sync.WaitGroupが必要になります。
goroutineとは
goroutineはGo言語固有の並行処理機能であり、通常の関数呼び出しが行われる際にgoキーワードを使用することで起動します。この方法で複数のタスクを同時に実行できるため、処理効率が向上します。
注意点:goroutineは起動後、メインスレッドとは別に動作するため、適切な同期処理が必要です。不適切な処理ではデータ競合やデッドロックなどの深刻なバグが発生します。
channelの役割
channelはgoroutine間でデータを送受信するために使用される通信手段です。これにより、複数のgoroutineが同じデータに安全にアクセスできるようになります。
チャネルの基本操作と種類
| 項目 | 値 | 補足 |
|---|---|---|
| チャネル作成 | make(chan 型) |
無限バッファリング(同期通信)またはバッファ付き(非同期) |
| 送信操作 | ch <- 値 |
受信者がいない場合はブロッキングされる |
| 受信操作 | 値 := <-ch |
送信者がいない場合は無限に待機する |
シンプルなチャネル使用例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package main import "fmt" func main() { ch := make(chan string) go func() { ch <- "こんにちは" }() msg := <-ch fmt.Println(msg) // 出力: こんにちは } |
sync.WaitGroupの必要性と使い方
複数のgoroutineが並行して実行される場合、メインスレッドはそれらの完了を待つ必要があります。sync.WaitGroupを使うことで、すべてのgoroutineが終了するまで待機できます。
WaitGroupライフサイクルの手順
Add(n)で待機するgoroutine数を指定- 各goroutineで
Done()を呼び出して完了通知 - メインスレッドは
Wait()で全完了を待つ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() fmt.Println("タスク1実行中") }() go func() { defer wg.Done() fmt.Println("タスク2実行中") }() wg.Wait() } |
goroutineの基本構文と実装例
goroutineは簡単な関数呼び出しによって実現できます。goキーワードで起動させることで、並列処理が可能になります。
並列化の重要性
- 順序のランダム性:goroutineは別のスレッド上で動作するため、出力順序は予測不能
- 同期の必要性:デッドロックやデータ競合を防ぐため、WaitGroupやchannelを使用して明示的に同期処理を行う
関数呼び出しの並列化
|
1 2 3 4 5 6 7 8 9 |
func greet() { fmt.Println("こんにちは") } func main() { go greet() fmt.Println("メインスレッド実行中") } |
注意:このコードでは
select{}による無限待ちが使用されていますが、実際にはsync.WaitGroupで終了待ちを行うのが一般的です。
並行処理におけるデッドロック回避ガイド
並行処理中に発生するデッドロックは、プログラムの停止につながる深刻な問題です。特にチャネル操作を誤ると簡単に発生します。
デッドロック発生の典型例
|
1 2 3 |
ch := make(chan string) ch <- "データ" // 送信元がないため、無限に待機 |
バッファードチャンネルの仕組み
バッファードチャネルは容量を指定できるため、送信元と受信先が同時に動作していない場合でも一時的に保存可能です。この仕組みによりデッドロック回避や非同期通信が可能になります。
|
1 2 3 4 5 6 7 8 |
ch := make(chan string, 1) // バッファサイズ1のチャネル go func() { ch <- "データ" }() fmt.Println(<-ch) |
実世界での並行処理応用例
Go言語で並行処理が役立つ具体例は、I/Oバウンド処理や同時実行タスクなどです。以下では画像処理とAPI呼び出しを例に挙げます。
同時実行タスクの制御
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package main import ( "fmt" "sync" ) func processImage(img string, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("%sの処理完了\n", img) } func main() { var wg sync.WaitGroup images := []string{"image1.jpg", "image2.png", "image3.gif"} for _, img := range images { wg.Add(1) go processImage(img, &wg) } wg.Wait() } |
I/Oバウンド処理の最適化
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import ( "fmt" "sync" ) func fetchAPI(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("API %d の取得完了\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Add(1) go fetchAPI(i, &wg) } wg.Wait() } |
まとめ
| 項目 | 内容 |
|---|---|
| goroutine | 軽量スレッドで並列処理可能 同期処理が必須 |
| channel | goroutine間のデータ共有に使用 ブロッキングとバッファリングが利用可能 |
| sync.WaitGroup | 複数goroutineの終了を待つためのメカニズム |
Go言語における並行処理は、これらを理解することでより効率的なプログラミングが可能になります。実際のコードを書いてみることで、さらに深い理解が得られます。