並列処理・並行処理

Go言語におけるgoroutine終了待ちの実装方法について解説

この記事では、Go言語で複数のgoroutineの終了待ちを簡単に行う方法について説明します。

具体的には、sync.WaitGroupなどの基本的な手法を用いて、並行処理の実装例を交えながら効率的な終了管理の方法を紹介します。

この記事を通じて、ゴルーチンの制御をよりスムーズに行う手法を身につけていただければと思います。

sync.WaitGroupを用いた終了待ちの実装

この節では、sync.WaitGroupを利用してgoroutineの終了を待つ方法について解説します。

コード例を交えながら、基本操作と実装の流れ、注意点を説明します。

sync.WaitGroupの基本操作

sync.WaitGroupは、複数のgoroutineの終了を待つための仕組みです。

Add, Done, Waitの3つの基本メソッドを用いて管理を行います。

Add, Done, Waitの動作説明

Addは待機するgoroutineの数を登録するために使用します。

Doneはgoroutineの処理が完了したことを通知し、内部カウンターを減少させます。

そしてWaitは、内部カウンターが0になるまで処理をブロックします。

以下は、sync.WaitGroupの基本動作を示すサンプルコードです。

// サンプルコード: sync.WaitGroupの基本操作例
package main
import (
	"fmt"
	"sync"
)
func main() {
	// WaitGroupのインスタンス作成
	var wg sync.WaitGroup
	// 2つのゴルーチンが完了するのを待つために2を追加
	wg.Add(2)
	// 1つ目のゴルーチンを起動
	go func() {
		// 処理完了時にカウンターを減らす
		defer wg.Done()
		fmt.Println("ゴルーチン1終了")
	}()
	// 2つ目のゴルーチンを起動
	go func() {
		defer wg.Done()
		fmt.Println("ゴルーチン2終了")
	}()
	// すべてのゴルーチンが終了するまで待機
	wg.Wait()
	fmt.Println("すべてのゴルーチン終了")
}
ゴルーチン1終了
ゴルーチン2終了
すべてのゴルーチン終了

実装の流れと注意点

sync.WaitGroupを利用する際の基本的な流れは以下の通りです。

  • メイン関数または管理する関数内でWaitGroupのインスタンスを作成する。
  • goroutineを起動する前に、Addで待機するgoroutine数を正確に設定する。
  • 各goroutine内で、処理開始直後または終了直前にdefer wg.Done()を呼び出して、必ずカウンターが減るようにする。
  • メイン関数でWaitを呼び出し、全てのgoroutineが終了するのを待つ。

注意点として、Addの呼び出しタイミングが遅れると、goroutineがすぐに終了してしまい意図した待機が行われない可能性があります。

また、必ずDoneを呼び出すようにして、カウンターが適切に管理されることを確認してください。

goroutineの起動と終了管理

この節では、goroutineの起動方法と、各goroutineが終了したことを通知する手順について説明します。

goroutineの起動方法

goroutineは、goキーワードを使用して関数を非同期に実行することで起動されます。

以下のように、関数リテラルを利用して書くことが一般的です。

// サンプルコード: goroutineの起動例
package main
import (
	"fmt"
)
func main() {
	// 単純なgoroutineの起動
	go func() {
		fmt.Println("新しいゴルーチンが実行中")
	}()
	// 実行順は保証されないため、メイン処理が終了しないように工夫が必要
	fmt.Scanln() // 入力待ちでプログラム終了を防止
}
新しいゴルーチンが実行中

終了通知の実装手順

goroutineの終了通知は、sync.WaitGroupを使って実装する方法がシンプルです。

すでに前述した例でも解説したように、各goroutine内でdefer wg.Done()を用いて終了を通知します。

具体的な手順は以下の通りです。

  • goroutine起動前に、wg.Add(1)または必要な数だけカウンターを増やす。
  • goroutine内でdefer wg.Done()を設定し、処理が終了する際に必ず通知する。
  • メインの処理でwg.Wait()を呼び出し、全ての通知が得られるまで待機する。

以下は、終了通知を明示的に行う例です。

// サンプルコード: 終了通知の実装例
package main
import (
	"fmt"
	"sync"
	"time"
)
func main() {
	var wg sync.WaitGroup
	// 1つのgoroutineに対してカウンターを増加
	wg.Add(1)
	go func() {
		defer wg.Done() // 終了時に通知
		fmt.Println("処理中...")
		time.Sleep(1 * time.Second)
		fmt.Println("処理完了")
	}()
	// すべてのgoroutineが終了するまで待機
	wg.Wait()
	fmt.Println("すべての終了通知を受信")
}
処理中...
処理完了
すべての終了通知を受信

チャネルを利用した終了待ち手法

この節では、チャネルを使った終了待ちの手法について説明します。

チャネルを利用することで、複数のgoroutineの終了通知や複数のシグナルを効率的に受け取ることが可能です。

チャネルによる通知機構の概要

チャネルは、ゴルーチン間でデータをやり取りするための仕組みです。

終了通知の場合、各goroutineからシグナルをチャネルに送信し、メインルーチンでその値を受け取る方法が利用されます。

例えば、各goroutineが処理完了時にtrueなどの値を送信し、指定した数の通知が揃ったら次の処理へ進みます。

シンプルなチャネル実装例

以下は、チャネルを用いて複数のgoroutineの終了を待つサンプルコードです。

// サンプルコード: シンプルなチャネル実装例
package main
import (
	"fmt"
	"time"
)
func main() {
	// 終了通知用のチャネルを作成。バッファサイズはgoroutine数に合わせる
	done := make(chan bool, 2)
	// 1つ目のgoroutineを起動
	go func() {
		fmt.Println("ゴルーチン①処理開始")
		time.Sleep(500 * time.Millisecond)
		fmt.Println("ゴルーチン①処理完了")
		done <- true // 終了通知を送信
	}()
	// 2つ目のgoroutineを起動
	go func() {
		fmt.Println("ゴルーチン②処理開始")
		time.Sleep(700 * time.Millisecond)
		fmt.Println("ゴルーチン②処理完了")
		done <- true // 終了通知を送信
	}()
	// 2つの終了通知を受信するまで待機
	<-done
	<-done
	fmt.Println("すべてのゴルーチンが終了")
}
ゴルーチン①処理開始
ゴルーチン②処理開始
ゴルーチン①処理完了
ゴルーチン②処理完了
すべてのゴルーチンが終了

select文との組み合わせによる制御

select文を用いると、複数のチャネルからのデータ受信やタイムアウト処理、優先順位を付けた制御が可能となります。

例えば、goroutineの終了通知と同時にタイムアウトを設定する場合、以下のように実装することができます。

// サンプルコード: select文とチャネルを組み合わせた例
package main
import (
	"fmt"
	"time"
)
func main() {
	done := make(chan string, 1) // 終了通知用のチャネル
	go func() {
		// 長い処理をシミュレーション
		time.Sleep(2 * time.Second)
		done <- "ゴルーチン終了"
	}()
	// select文を利用して終了通知かタイムアウトを待機
	select {
	case msg := <-done:
		fmt.Println(msg)
	case <-time.After(1 * time.Second):
		fmt.Println("タイムアウト:処理が時間内に終了しませんでした")
	}
}
タイムアウト:処理が時間内に終了しませんでした

並行処理実装時の注意点とパフォーマンス向上策

この節では、並行処理実装時に注意すべきエラー処理やデッドロック回避の方法、パフォーマンス向上のためのポイントについて説明します。

エラー処理と例外管理のポイント

並行処理では、各goroutineで発生するエラーが想定外の動作を引き起こす可能性があるため、エラー処理は重要です。

ここでは、各goroutineからエラー情報をチャネルで返し、メインで収集して適切に処理する方法を示します。

適切なエラーハンドリングの実践

以下のサンプルコードは、複数のgoroutineからエラーを収集し、まとめて処理する方法を示しています。

// サンプルコード: エラーハンドリングの実践例
package main
import (
	"errors"
	"fmt"
	"time"
)
func worker(id int, errChan chan error) {
	// サンプル処理。idが偶数の場合にエラーを返す
	time.Sleep(time.Duration(id) * 100 * time.Millisecond)
	if id%2 == 0 {
		errChan <- errors.New(fmt.Sprintf("worker %dでエラー発生", id))
		return
	}
	errChan <- nil
}
func main() {
	numWorkers := 4
	errChan := make(chan error, numWorkers)
	// 複数のworkerごとにgoroutineを起動
	for i := 1; i <= numWorkers; i++ {
		go worker(i, errChan)
	}
	// 各workerからのエラーを収集
	for i := 1; i <= numWorkers; i++ {
		err := <-errChan
		if err != nil {
			fmt.Println("エラー:", err)
		} else {
			fmt.Println("worker", i, "は正常終了")
		}
	}
	fmt.Println("すべてのworker処理完了")
}
worker 1 は正常終了
エラー: worker 2でエラー発生
worker 3 は正常終了
エラー: worker 4でエラー発生
すべてのworker処理完了

デッドロック回避の工夫

複数のgoroutine間でチャネルやリソースを共有していると、デッドロックのリスクが高まります。

適切なリソース管理とタイミング調整でこれを回避するための注意点を解説します。

リソース管理とタイミング調整のコツ

デッドロックを回避するためのコツは以下の点です。

  • チャネルに対して正しいバッファサイズを設定し、送信と受信の順序を意識する。
  • goroutine間でロック(sync.Mutexなど)を使用する際は、ロック獲得の順序を統一する。
  • タイムアウト機構を導入し、一定時間内に処理が完了しなかった場合に処理を打ち切る。

以下は、タイムアウトを利用してデッドロックを回避する例です。

// サンプルコード: タイムアウトを利用したデッドロック回避例
package main
import (
	"fmt"
	"time"
)
func longProcess(done chan bool) {
	// 処理に時間がかかる場合をシミュレート
	time.Sleep(3 * time.Second)
	done <- true
}
func main() {
	done := make(chan bool, 1)
	go longProcess(done)
	// select文でタイムアウトを設定して待機
	select {
	case result := <-done:
		if result {
			fmt.Println("処理完了")
		}
	case <-time.After(1 * time.Second):
		fmt.Println("タイムアウト:処理が遅延中")
	}
}
タイムアウト:処理が遅延中

上記の例では、長い処理に対して1秒のタイムアウトを設けています。

タイムアウトを活用することで、意図しないデッドロックの発生を防止できます。

まとめ

本記事では、sync.WaitGroupやチャネルを利用したgoroutineの終了待ち手法、エラー処理やデッドロック回避の工夫について具体例を交えて解説しました。

並行処理実装における基本操作と注意点が把握できる内容となっています。

ぜひ実際にサンプルコードを試し、より効率的なGoプログラム作成に挑戦してみてください。

関連記事

Back to top button
目次へ