はじめてのGo―シンプルな言語仕様,型システム,並行処理

第5章 並行プログラミング―ゴルーチンとチャネルを使いこなす

この記事を読むのに必要な時間:およそ 7 分

sync.WaitGroup

先ほどの例ではtime.Sleep()main()を1秒間待たせていましたが,実際に待ちたいのはhttp.Get()を行っているすべてのゴルーチンの終了です。

起動したすべてのゴルーチンの終了を待ち合わせるにはsync.WaitGroupが利用できます。sync.WaitGroupは,Add()でカウントを増やしDone()でカウントを減らし,Wait()でカウントがゼロになるまで待ち合わせます。

func main() {
    wait := new(sync.WaitGroup)
    urls := []string{
        "http://example.com",
        "http://example.net",
        "http://example.org",
    }
    for _, url := range urls {
        // waitGroupに追加
        wait.Add(1)
        // 取得処理をゴルーチンで実行する
        go func(url string) {
            res, err := http.Get(url)
            if err != nil {
                log.Fatal(err)
            }
            defer res.Body.Close()
            fmt.Println(url, res.Status)
            // waitGroupから削除
            wait.Done()
        }(url)
    }
    // 待ち合わせ
    wait.Wait()
}

チャネル

複数のゴルーチン間でデータをやりとりしたい場合,組込みのチャネルchannelという機能を用いることで,メッセージパッシング(情報をメッセージとして送受信する)によってデータを送受信できます。チャネルはmake()関数に型を指定して生成することで,その型のデータの書き込みと読み出しができます。

// stringを扱うチャネルを生成
ch := make(chan string)

// チャネルにstringを書き込む
ch <- "a"

// チャネルからstringを読み出す
message := <- ch

今回の場合は,ゴルーチン内で取得したステータスコードをチャネルに書き込み,それをmain()のゴルーチンで読み出すことで,ゴルーチン間でデータを受け渡すことができます。

func main() {
    urls := []string{
        "http://example.com",
        "http://example.net",
        "http://example.org",
    }
    statusChan := make(chan string)
    for _, url := range urls {
        // 取得処理をゴルーチンで実行する
        go func(url string) {
            res, err := http.Get(url)
            if err != nil {
                log.Fatal(err)
            }
            defer res.Body.Close()
            statusChan <- res.Status
        }(url)
    }
    for i := 0; i < len(urls); i++ {
        fmt.Println(<-statusChan)
    }
}

ゴルーチンの中でstatusChanに値が書き込まれるまで,main()の中では値を読み出すことができません。この場合,main()内ではstatusChanの読み出しが3回完了するまで処理がブロックされるため,waitGroupのような待ち合わせ処理は必要ありません。

これにより,HTTPリクエストを並行して発行し,早く取得されたステータスから順に受け取ることができます図3⁠。

図3 チャネルによる値の受け渡し

図3 チャネルによる値の受け渡し

チャネルを返すパターン

先ほどはmain() 内の匿名関数でHTTPのGETを実行していましたが,この処理をgetStatus()という別の関数にし,関数が内部で生成したチャネルを返すように実装してみます。

func getStatus(urls []string) <-chan string {
    // 関数でチャネルを生成
    statusChan := make(chan string)
    for _, url := range urls {
        go func(url string) {
            res, err := http.Get(url)
            if err != nil {
                log.Fatal(err)
            }
            defer res.Body.Close()
            statusChan <- res.Status
        }(url)
    }
    return statusChan // チャネルを返す
}

func main() {
    urls := []string{
        "http://example.com",
        "http://example.net",
        "http://example.org",
    }
    statusChan := getStatus(urls)

    for i := 0; i < len(urls); i++ {
        fmt.Println(<-statusChan)
    }
}

まず,getStatus()内で結果を渡すためのstatus Chanを生成します。次に非同期に行う処理を匿名関数にし,リクエストをそれぞれ別のゴルーチンで実行します。関数自体はstatusChanを返して終了し,起動されたゴルーチンが内部でstatusChanに結果を書き込んでいきます。

main()は,関数を呼び出すと同時に結果を受信するチャネルを受け取り,それをfor文内で読み出します。これにより,main()側が非常にスッキリと記述でき,ロジックの大半はgetStatus()に隠蔽(いんぺい)できました。

また,このときgetStatus()main()がチャネルに値を書き込むことを想定していません。こうした場合は,getStatus()の戻り値を<-chan stringと読み出し専用のチャネルにすることで,main()がこのチャネルに値を書き込むことを型レベルで防ぐことができます。

このパターンはチャネルを用いる場合によく使うので,覚えておくとよいでしょう。

COLUMN:GOMAXPROCS

現在のGo 1.3では,複数のゴルーチンを起動しても,マルチコアを自動的に使い切るような最適化はされていません。したがって,ゴルーチンを複数のコアで実行したい場合は実行時にGOMAXPROCS環境変数を指定するか,runtime.GOMAXPROCS()にコア数を指定する必要があります。

// CPU数を取得
cpus := runtime.NumCPU()

// ランタイムが使用するCPU数を指定
runtime.GOMAXPROCS(cpus)

著者プロフィール