0
0

More than 1 year has passed since last update.

【Go】学習メモ② ~Goroutine, Channel~

Last updated at Posted at 2021-12-26

はじめに

前回の学習メモの続きです。
GoroutineとChannelについてまとめました。

シリアル処理

本題の前に、シリアル処理についてまとめます。
まずは以下のように、5つのリンクにhttpリクエストが正常に送られるかどうかをチェックするコードを考えます。

main.go
func main() {
    links := []string {
        "http://google.com",
        "http://facebook.com",
        "http://stackoverflow.com",
        "http://golang.org",
        "http://amazon.com",
    }

    for _, link := range links {
        checkLink(link)
    }
}

func checkLink(link string) {
    _, err := http.Get(link)
    if err != nil {
        fmt.Println(link, "might be down!")
        return
    }

    fmt.Println(link, "is up!")
}

このとき、forループで各リンクにリクエストを送るため、レスポンスも一個ずつ返ってきます。
これをシリアル(直列)処理といいます。

もしすべてのリクエストを同時に送ることができれば、他のリクエストのレスポンスを待つことなく、同時にレスポンスを取得することができます。
このような処理を行うために、GoにはGoroutineやChannelといったものが存在します。

Goroutine(ゴルーチン)

GoroutineはGoのプログラムにおける最小の構成単位で、Goランタイムによって管理される軽量な並行処理スレッドのことをいいます。
main()で実行される処理もGoroutineにあたり、これをMain Goroutine(メインゴルーチン)と呼びます。
Goroutineでは、スレッド数やメモリの管理などの複雑な作業がGoランタイムで管理され、開発者による制御を必要としません。
そのため、開発者は複雑な制御を考えることなく、実装に注力することができます。

Goroutineはgo checkLink(link)のように、関数の前にgoをつけてあげることで定義することができます。
Main Gotoutineに対して、これらをChild Goroutineと呼びます。

Channel(チャネル)

シリアル処理のコードのcheckLink(link)の箇所をgo checkLink(link)と書き換えて実行すると、実行結果には何も表示されません。
これはただChild Goroutineを定義しただけだと、Main GoroutineがChild Goroutineの実行状態などを気にせずに終了してしまうためです。

このときに使用するのがChannelです。
Channelは、同時実行するGoroutineを接続するパイプのようなもので、あるGoroutineからChannelに値を送信すると、それらの値を別のGoroutineで受け取ることができます。

Channelを使うことで、Main Goroutine側からChild Goroutineの実行が終了したかどうかを確認することができるようになります。

また、Channelにおけるデータの取り扱いの例は以下のようになります。

  • channel <- 5: Channelに5を送信する
  • myNumber <- channel: Channelに送られる値を待ち、値が得られたらその値をmyNumberに代入する
  • fmt.PrintLn(<- channel): Channelに送られる値を待ち、値が得られたらその値をログ出力する

実装

GoroutineとChannelについて確認したところで、並行処理の実装に戻ります。

試しに、c := make(chan string)でChannelを作成してcheckLink()に引数で渡し、c <- ~でChannelに送信した値を出力するようなコードを書いてみます。

main.go
func main() {
    links := []string {
        "http://google.com",
        "http://facebook.com",
        "http://stackoverflow.com",
        "http://golang.org",
        "http://amazon.com",
    }

    c := make(chan string)

    for _, link := range links {
        go checkLink(link, c)
    }

    fmt.Println(<- c)
}

func checkLink(link string, c chan string) {
    _, err := http.Get(link)
    if err != nil {
        fmt.Println(link, "might be down!")
        c <- "Might be down I think"
        return
    }

    fmt.Println(link, "is up!")
    c <- "Yep its up"
}

すると実行結果には1つのリンクしか表示されません。

http://stackoverflow.com is up!
Yep its up

これはChannelにデータが送信された時点でfmt.Println(<- c)が実行されてしまい、Main Goroutineが終了してしまうためです。
試しにfmt.Println(<- c)を2つに増やすと、今度は以下のようにリンクが2つ表示されます。

http://google.com is up!
Yep its up
http://stackoverflow.com is up!
Yep its up

つまり、すべてのリンクから送られるChannelのデータを取得するには、以下のようにリンクの数だけforループしてあげればOKです。

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

次に、ステータスチェッカーとして各リンクの応答を常時確認するような実装を考えると、以下のようにChannelから送信されたデータを再度forループ内のgo checklink()で受け取るような形になります。

    for {
        go checkLink(<-c, c)
    }

こちらは糖衣構文です。

    for l := range c {
        go checkLink(l, c)
    }

ただ、これだと結果が絶え間なくステータスをチェックし続けてしまいます。

5つのリンクのレスポンスを取得する前に5秒待つような処理に変えてみます。
無名関数をつくり、内部でtime.Sleep(5 * time.Second)を記述します。

    for l := range c {
        go func() {
            time.Sleep(5 * time.Second)
            checkLink(l, c)
        }()
    }

すると、2回目のレスポンスがすべてhttp://amazon.com is up!になってしまいました...

<1回目>
http://google.com is up!
http://stackoverflow.com is up!
http://facebook.com is up!
http://golang.org is up!
http://amazon.com is up!
<2回目>
http://amazon.com is up!
http://amazon.com is up!
http://amazon.com is up!
http://amazon.com is up!
http://amazon.com is up!

これは、checkLink()の引数として与えられているlがループ変数のlによってキャプチャされていることで起こります。
ループ変数にキャプチャされていると、無名関数が実行されたとき(Goroutineがスケジュールされたとき)にループ変数の値を読みにいってしまいます(Main GoroutineとChild Goroutineで同じメモリアドレスの値を読んでしまう)。
そのため、lを無名関数にコピーして引数に渡してあげる(即時関数にする)ことでこの問題を解決することができます。

    for l := range c {
        go func(link string) {
            time.Sleep(5 * time.Second)
            checkLink(link, c)
        }(l)
    }

これで5秒おきに5つのリンクのステータスをチェックできるようになりました。

備考

最終コードの全文は以下のようになります。

main.go
package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    links := []string {
        "http://google.com",
        "http://facebook.com",
        "http://stackoverflow.com",
        "http://golang.org",
        "http://amazon.com",
    }

    c := make(chan string)

    for _, link := range links {
        go checkLink(link, c)
    }

    for l := range c {
        go func(link string) {
            time.Sleep(5 * time.Second)
            checkLink(link, c)
        }(l)
    }
}

func checkLink(link string, c chan string) {
    _, err := http.Get(link)
    if err != nil {
        fmt.Println(link, "might be down!")
        c <- link
        return
    }

    fmt.Println(link, "is up!")
    c <- link
}

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0