Goの並行処理 -基本のキ-

このエントリーは、エキサイト Advent Calendar 2016の12/03の記事です。
エキサイトとしては初のAdvent Calendar参戦です!めでたい🏢🎊👏🐟!!
こんにちは!✌
エキサイト婚活の担当エンジニア、新卒1年目の牧山です。
僭越ながら、3日目を担当させていただきます!お手柔らかにお願いします^^

入社から半年と少し、フロントエンドからバックエンドまで幅広く経験させて頂き、
そこで得た知識を今回まとめて記事、には、しません!!

今回はGoについて簡単に触れさせていただきます!ʕ◔ϖ◔ʔ!
社外向け記事とはいえ、社内の方の目にも多く触れる場…
せっかくの機会なので何かのきっかけになればと思います。
それよりなによりGoが言語として好きというのが大きいのは内緒です。

はじめに

本記事では、Goの文法やパッケージの使い方などに関しての紹介は
あえて割愛させていただきます。
読者の方に、Goでの並行処理がいかにシンプルな記述かを感じ取っていただくとともに
特有の記述などに関して興味を持ってもらい、自ら触れていただければ作戦成功です😂
あえて無名関数やstructの定義、ポインタなどいろいろ多用しているのも内緒です。

Go(Golang)とは

Goの並行処理 -基本のキ-_f0364156_17415422.jpg
Goは2009年にGoogleが発表したオープンソースのプログラミング言語です。
最近ではGoを採用した事例も耳新しくなくなってきています。

Playgroundを利用したA Tour of Goを一通りやれば
基本構文はある程度理解できるかと思います。シンプルです。素敵。

言語の特徴

Goには以下のような特徴があります。
  • 優れたパフォーマンス
  • コンパイル/実行速度が早い
  • 言語仕様がシンプル
  • クロスコンパイルをサポート
  • 並行処理のサポート
  • マスコットがかわいい
  • etc…
Goといえばよく、実行速度の速さにフォーカスが当てられ多くの比較記事を目にします。
実際に早いのですが、比較に関しては他記事を参考にするのがよいでしょう。

上記のような特徴から、事例としてはバックエンドのAPIやバッチ処理が多いようです。

Goによる並行処理

PHPにはマルチスレッドの概念がないため、並行処理をするには職人芸が必要です。
一方でGoで、言語自体が並行処理に必要な機能をサポートしています。
ゴルーチンやチャネルを用いて比較的簡単に並行処理を実装できます。

goroutine(ゴルーチン)の基本

Goの並行処理 -基本のキ-_f0364156_12143904.jpg
Goの並行処理に欠かせないのがgoroutineです。
これは任意の関数をGoのランタイムの中で並行実行できるようしてくれるものです。
goroutineは軽量スレッドとなっており、実は、最初に実行される
main()関数も1つのgoroutineなのです。💁「ここテストに出ます」
使い方は簡単で、実行する関数にgoのキーワードを付与するだけです。

goroutineを使わない場合

例題では、3つの簡単なメソッドを記述しています。
  • 1秒WaitしてHello, worldを出力
  • Webにアクセスし、ステータスコードを出力
  • 引数で受けたNameを出力
package main

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

func helloWorld() {
time.Sleep(time.Second)
fmt.Println("Hello, world!!")
}

func getHttp(url string) {
res, err := http.Get(url)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
fmt.Println(url, res.Status)
}

func helloName(name string) {
fmt.Println("Hello", name)
}

func main() {
helloWorld()
getHttp("https://jobs.excite.co.jp/")
helloName("Makiyama")
time.Sleep(time.Second * 2)
fmt.Println("Done.")
}

実行結果は以下のようになります。

$ go run goroutine.go
Hello, world!!
https://jobs.excite.co.jp/ 200 OK
Hello Makiyama
Done.

3つのメソッドはそれぞれ独立した処理のため、順番に実行する必要がありません。
こうした場合、goroutineの登場です。

goroutineを使った場合

先述したように、非同期で処理したい関数の前にgoをつけるだけとなります。

/*-- 略 --*/

func main() {
go helloWorld()
go getHttp("https://jobs.excite.co.jp/")
go helloName("Makiyama")
time.Sleep(time.Second * 2)
fmt.Println("Done.")
}

実行結果や以下に!

$ go run goroutine.go
Hello Makiyama
https://jobs.excite.co.jp/ 200 OK
Hello, world!!
Done.

それぞれが別のgoroutine上で処理されていることが見て取れます。
こんなに簡単だとなんだかワクワクしますね!!

WaitGroupで複数の処理を待つ

疑問に思った方もいるかも知れません。
「main()関数でのtime.Sleep(time.Second * 2) is 何?」と。
冒頭でも触れたとおり、main()関数も1つのgoroutineの中で実行されています。
そのため、main()関数内で3つのgoroutineを起動し処理する間にも
main()の処理は行われているのです。先のサンプルからSleepの処理を外すと、

$ go run goroutine.go
Done.

という悲しい結果に。
しかし、実際の処理は何秒で終わるか未知なのが常。
そこでsyncパッケージのWaitGroupの登場です。
ランダムなタイミングで終了するgoroutineを5つ起動し、
それらすべての終了を待つ処理を例にあげます。

package main

import (
"fmt"
"log"
"math/rand"
"sync"
"time"
)

func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // goroutine起動のたびにインクリメント
go func(i int) {
defer wg.Done() // 関数終了時にデクリメント
n := rand.Intn(5)
log.Println("sleep for", n, "seconds", "ID:", i)
time.Sleep(time.Duration(n) * time.Second)
log.Println("wake up,", "ID:", i)
}(i)
}
wg.Wait() // 処理をブロックし、カウンタが0になったら次に進む
fmt.Println("Done.")
}

WaitGroupは厳密にどのgoroutineが終了したかは管理せず
単純にカウンタを保持し、その数が0になるまで待つことのできるオブジェクトです。

結果は以下のようになります。

$ go run waitGroup.go
2016/11/29 13:35:51 sleep for 1 seconds ID: 4
2016/11/29 13:35:51 sleep for 2 seconds ID: 1
2016/11/29 13:35:51 sleep for 4 seconds ID: 0
2016/11/29 13:35:51 sleep for 2 seconds ID: 2
2016/11/29 13:35:51 sleep for 1 seconds ID: 3
2016/11/29 13:35:52 wake up, ID: 4
2016/11/29 13:35:52 wake up, ID: 3
2016/11/29 13:35:53 wake up, ID: 1
2016/11/29 13:35:53 wake up, ID: 2
2016/11/29 13:35:55 wake up, ID: 0
Done.

channel(チャネル)の基本

Goの並行処理 -基本のキ-_f0364156_12145892.jpg
Goは、複数のgoroutine間でデータのやりとりを行うchannel機能を有しています。
channelは基本的に読み書き両方に利用できるソケットのようなイメージのものです。

channelは参照型となっており、make()関数に型を指定して生成します。
使い方はこれまた簡単で、送信がchannel<-valueで受信が<-channelです。

channelを用いたメッセージング

channelは、送受信が完了するまでブロックします。

package main

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

func getHttp(url string) <-chan string {
status := make(chan string) // channelの生成
go func(url string) {
res, err := http.Get(url)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
status <- res.Status // channelにデータを書き込み
}(url)
return status
}

func main() {
status := getHttp("https://jobs.excite.co.jp/")
fmt.Println(<-status) // channelにデータが書き込まれるまでブロック
fmt.Println("Done.")
}

上記の例では、関数内でchannelを生成し、返却しています。
main()関数ではchannelに値が書き込まれるまで先の処理をブロックする事になります。
その為、結果としてステータスを出力したあと、Doneが出力されます。

複数のchanに対する読み出しや書き込みを制御する方法として
slect文が用意されていますが、今回はその説明は割愛させていただきます。

channelによる排他制御

channelに対する操作はGoのランタイムにより、自動的に排他制御がなされています。
言語自体にこのような仕組みが組み込まれているため、
開発者は書き込み・読み込みでの排他制御、その他の複雑な作業をしなくてすむのです。

例として、A君とBさんが新年を祝うシチュエーションを考えてみます。(ありえない)
わかりにくい例なのはさておきコードは以下のようになります。

package main

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

type CountDown struct {
Count int
HappyWord string
}

func happyNewYear(started int, word string) {
var cd CountDown = CountDown{started, word}
aKun := make(chan *CountDown)
bSan := make(chan *CountDown)

var wg sync.WaitGroup
wg.Add(2)

go func() {
defer wg.Done()
for {
cd, ok := <-aKun // aKunから読み込み(書き込みを待つ)
if !ok {
break
}
time.Sleep(time.Second)
fmt.Printf(" A君「%d!!!」\n", cd.Count)
cd.Count-- // データの書き換え
if cd.Count == 0 {
break
}

bSan <- cd // bSanへ書き込み
}
close(bSan)
}()

go func() {
defer wg.Done()
for {
cd, ok := <-bSan // bSanから読み込み(書き込みを待つ)
if !ok {
break
}
time.Sleep(time.Second)
fmt.Printf("Bさん「%d!!!」\n", cd.Count)
cd.Count-- // データの書き換え
if cd.Count == 0 {
break
}

aKun <- cd // aKunへ書き込み
}
close(aKun)
}()

aKun <- &cd // 最初の書き込み
wg.Wait() // 終了を待つ
time.Sleep(time.Second)
fmt.Printf("A君・Bさん「「%s!!!」」\n", cd.HappyWord)
}

func main() {
happyNewYear(3, "Happy New year!!!")
fmt.Println("Done.")
}

上記の例では、aKunとbSanのchannelを通してCountDown型の変数をやりとりし、
そこに格納されている値をそれぞれが変更しています。
これを実行した結果が次になります。
Goの並行処理 -基本のキ-_f0364156_12061228.gif

このように、channelを複数のgoroutineで共有すれば、
簡単かつ安全にデータの共有ができます。

Goではこの方法を推奨しており、

Do not communicate by sharing memory;
instead, share memory by communicating.


とのスローガンを掲げているようです。

channelによる暗黙的な排他制御は便利ですが、それだけでは足りないことも出てきます。
その場合は明示的な排他制御を行う必要があり、sync.Mutex等を用いて実現できます。
これについては今回は割愛させていただきます。

まとめ

Goの並行処理 -基本のキ-_f0364156_12150863.jpg
Gopherがかわいい。で締めるわけにもいかないですね。

今回はGoの並行処理にフォーカスして説明させていただきました。
goroutineやchannelといったGo特有の記述をかいつまんだだけでも
プログラミング楽しい!となるのは私だけでしょうか。w
並行処理をシンプルに記述できるGoに魅力を感じていただけたら幸いです。

来年のエキサイト Advent Calendarでは、バッチ処理のひとつだけでも
Goを本番投入したった!という記事を書ければなあと思う次第です。


なお、GopherのオリジナルのデザインはRenee Frenchさんです
※ The Go gopher was designed by Renee French.
  (http://reneefrench.blogspot.com/)

明日は再び新卒1年目の登場、永野くんです!同期にはJSおじさんと呼ばれてたりも…
Google Analyticsに関して紹介してくれるそうです。積極的に参考にします(`・ω・´)ゞ

エンジニア募集

エキサイトでは一緒に働いてくださる方を新卒採用と中途採用で募集しています。
詳しくは、こちらの採用情報ページをご覧ください。
f0364156_13183704.png
エンジニアの働きやすい環境が整っており、風通しの良い会社です。
1年目でも「Go入れたい」とか勝手に言える素敵な会社です!
ぜひ一緒により良いサービスを作り上げていきましょう!ʕ◔ϖ◔ʔ!

by ex-engineer | 2016-12-03 00:00