見出し画像

Golang_ゴルーチン&チャネルとselectとsync.WaitGroup #471

Golangは、シンプルかつ強力な並行処理機能を提供します。並行処理の要であるゴルーチンについて以下の記事で整理しました。

この記事ではこれを更に深掘りし、並行処理を効率的に管理するために重要なselect構文とsync.WaitGroupについて詳しく解説します。

select構文

複数のチャネル操作を待機し、最初に準備ができた操作を実行するためのものです。これにより、非同期のチャネル通信を効率的に管理できます。以下が基本的なselect構文の例です。

select {
case : ch1 <- val
    // ch1への送信ができた(ch1を受信する準備ができた)場合の処理
case val := <-ch2:
    // ch2からの受信があった場合の処理
default:
    // どのケースも準備できていない場合の処理
}

例えば以下のように実装できます。

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:  // cを送信できたら(main()のforで「<-ch」が呼ばれたら)こちらに入る
			x, y = y, x+y
		case <-quit:  // main()のforを抜けて「quit <- 0」が走ったらこちらに入ってforを抜ける
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)  // チャネルから受信するまでは次のループに進まない
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

select構文では複数のチャネル操作を扱えますが、1つのチャネルの複数の状態を操作することもできます。先ほどのコードを編集し、チャネルquitを使わずにクローズしてみます。

package main

import "fmt"

func fibonacci2(c chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:        // cを送信できたら(main()のforで「<-ch」が呼ばれたら)こちらに入る
			x, y = y, x+y
		case _, ok := <-c:  // チャネルの状態を監視し、閉じられたら(main()でforを抜けてcloseされたら)forを抜ける
			if !ok {
				fmt.Println("quit")
				return	
			}
		}
	}
}

func main() {
	c2 := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Printf("2だぜ %d\n", <-c2)
		}
		close(c2)
	}()
	fibonacci2(c2)
}

これにより実装できる機能の幅が広がりそうですね。

sync.WaitGroup

こちらは複数のゴルーチンの完了を待つための同期プリミティブです。これにより、複数のチャネルが全てクローズされるのを待ってから処理を進める、という実装が可能になります。

以下が基本的な使用方法です。

(1) WaitGroupの宣言: 
 
WaitGroupを宣言します

var wg sync.WaitGroup

(2) カウントの追加: 
 カウントするゴルーチン数を定義します

wg.Add(n)

(3) ゴルーチンの終了を通知:
 ゴルーチン内で遅延実行を定義して終了を通知します

defer wg.Done()

(4) すべてのゴルーチンの完了を待機
 以下のコードで処理を停止し、カウンタが0になるまで待機します

wg.Wait()

具体的なコード例は以下です。

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 遅延実行でゴルーチンの終了を通知
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second) // 1秒間の作業をシミュレート
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // ゴルーチンの数をカウント
        go worker(i, &wg)
    }

    wg.Wait() // すべてのゴルーチンの終了を待機
    fmt.Println("All workers done")
}

goキーワードをつけて関数をゴルーチンで実行する直前でwg.Add(1)でカウンタを1つ増やしています。そして関数が終了するとwg.Done()が遅延実行され、カウンタが1つ減る仕組みです。

ここではwg.Add(1)が3回実行されるので、wg.Done()が3回実行されたタイミングでwg.Wait()が待機を終了し、次の処理に進みます。

これにより複数のゴルーチンを起動して並行性を高めつつ、それぞれの完了を待ってからの処理が可能になります。


ここまでお読みいただきありがとうございました!!

参考


この記事が気に入ったらサポートをしてみませんか?