goroutine の概要
コンカレンシーは、独立した複数のアクティビティの構成です。たとえば、Web サーバーにより、複数のユーザー要求が同時に、さらに自律的に処理されるときに実行される作業などです。 現在、コンカレンシーは多くのプログラムに存在します。 Web サーバーはその一例ですが、大量のデータを一括処理するにはコンカレンシーが必要であることもわかります。
Go には、同時実行プログラムを作成するためのスタイルが 2 つあります。 1 つは、他の言語で使ったことがあるかもしれませんが、スレッドを使用する従来のスタイルです。 このモジュールでは、プロセスをやり取りする goroutines という独立したアクティビティ間で値が渡される Go のスタイルについて説明します。
コンカレンシーについて初めて学ぶ場合は、実習用に作成するコードの個々の部分について、時間をかけて確認することをお勧めします。
コンカレンシーに対する Go のアプローチ
通常、並行プログラムを作成する際の最大の問題は、プロセス間でデータを共有することです。 Go では、チャネルを介してデータがやり取りされるため、通信を使用する他のプログラミング言語とはアプローチが異なります。 この手法が意味するところは、データにアクセスできるのは 1 つのアクティビティ (goroutine) のみであり、設計上、競合状態は発生しないということです。 このモジュールで goroutine とチャネルについて学ぶと、Go のコンカレンシー アプローチについて理解を深めることができます。
Go 手法をまとめると、そのスローガンは "メモリの共有で通信せず、通信でメモリを共有してください" というものになります。この手法については次のセクションで取り上げますが、Go のブログ記事「通信でメモリを共有する」にも詳細があります。
前述のように、Go には低レベルのコンカレンシー プリミティブも含まれています。 ただし、このモジュールでは、Go 独自のコンカレンシー手法のみを取り上げます。
まず、goroutine から見てみましょう。
goroutine
goroutine とは、オペレーティング システムでの従来のアクティビティではなく、軽量スレッドでの同時アクティビティです。 出力に書き込むプログラムと、2 つの数値の加算などを計算する別の関数があるとします。 1 つの同時実行プログラムに、両方の関数を同時に呼び出す複数の goroutine を含めることができます。
プログラムによって最初に実行される goroutine は main()
関数であると言えます。 別の goroutine を作成する場合は、次の例のように、関数を呼び出す前に go
キーワードを使用する必要があります。
func main(){
login()
go launch()
}
また、多くのプログラムでは、次のコードのように匿名関数を使用して goroutine を作成することが好まれています。
func main(){
login()
go func() {
launch()
}()
}
goroutine の動作を確認するために、同時実行プログラムを作成してみましょう。
同時実行プログラムを作成する
同時実行の部分のみに集中したいので、API エンドポイントが応答するかどうかを確認する既存のプログラムを使用しましょう。 コードは次のとおりです。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
for _, api := range apis {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
continue
}
fmt.Printf("SUCCESS: %s is up and running!\n", api)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
前述のコードを実行すると、次の出力が表示されます。
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 1.658436834 seconds!
内容に異常な点は何もありませんが、もっと適切な方法があります。 すべてのサイトを同時に確認できるのではないでしょうか? プログラムの終了までにかかる時間を約 2 秒ではなく、500 ms 未満にすることができそうです。
同時に実行する必要があるコードの部分は、サイトへの HTTP 呼び出しを行う部分であることに注目してください。 つまり、プログラムによって確認される API ごとに goroutine を作成する必要があります。
goroutine を作成するには、関数を呼び出す前に go
キーワードを使用する必要があります。 ただし、まだ関数がありません。 そのコードをリファクターし、次のように新しい関数を作成してみましょう。
func checkAPI(api string) {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
return
}
fmt.Printf("SUCCESS: %s is up and running!\n", api)
}
continue
キーワードは、for
ループ内に含まれていないため、不要になっていることに注目してください。 関数の実行フローを停止するには、return
キーワードを使用します。 次に、main()
関数のコードを変更して、次のように API ごとに goroutine を作成する必要があります。
for _, api := range apis {
go checkAPI(api)
}
プログラムを再実行して、何が起こるかを確認します。
プログラムによって API が確認されなくなったようです。 次の出力のようになるでしょうか。
Done! It took 1.506e-05 seconds!
高速でしたね。 何が起きましたか? Go によってループ内のサイトごとに goroutine が作成され、すぐに次の行に進んだため、プログラムが完了したことを示す最終メッセージが表示されます。
checkAPI
関数が実行されているようには見えませんが、実行されています。 完了する時間がなかっただけです。 次のように、ループの直後にスリープ タイマーを含めるとどうなるかについて注目してください。
for _, api := range apis {
go checkAPI(api)
}
time.Sleep(3 * time.Second)
このプログラムを再実行すると、次のような出力が表示されます。
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 3.002114573 seconds!
正常に動作しているようですね。 ただし、正確に、ではありません。 一覧に新しいサイトを追加すると、どうなるでしょうか? おそらく 3 秒では足りません。 どうすれば把握できるでしょうか? できません。 より良い方法が必要ですが、次のセクションで、チャネルについて取り上げる際に説明します。