Go勉強会(ゴルーチン・チャネル)のふりかえりと活用事例の紹介
はじめまして。フクロウラボでサーバサイドを担当しています渋谷と申します。よろしくお願いいたします。
はじめに
今回は、Go勉強会で学習した内容「ゴルーチン・チャネル」をふりかえりつつ、弊社での活用事例を紹介したいと思います。
フクロウラボでは @tenntenn さんの「Goハンズオン - ガチャを作ろう」を題材に、Go勉強会を毎週開催しています。2/17(木)と2/24(木)にSection 10: 並行処理」学びました。
@tenntenn さんありがとうございました🙇♂️
※注1:資料を使った講義等を行う場合は事前に@tenntennに許可を得る必要があります。
勉強会の様子は下記をご覧下さい。
Go勉強会のふりかえり
並行処理と並列処理について
曖昧になりやすいですが、並行処理と並列処理は別ものです。
これは Rob Pike 氏(Go言語の父)のBlogに記載されています。
ふむふむ?!
「Go 言語による並行処理」本の一文を見てみましょう。
イメージ図がわかりやすいですね。
並行処理でコードを書いても、動作環境(左図:単一プロセッサ)によっては、並列に処理できないということですね。
今回は触れませんが、「並行処理の難しさ」についても別の機会で深掘りできればと思います。
ゴルーチンについて
A Tour of Go の説明と、言語仕様を見てみましょう。
ふむふむ。
A Tour of Goのコードを確認してみましょう。
注2:for文の実行回数を5から2に変更しています。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 2; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
ソース:https://go.dev/tour/concurrency/1
[結果]
// 実行結果(1回目)
world
hello
world
hello
// 実行結果(2回目)
hello
world
world
hello
// 実行結果(3回目)
hello
world
hello
「hello」と「world」の出力順番が違います。また実行タイミングによって「world」が出力されないことがあります。
ゴルーチンには下記特徴があります。
処理順序が保証されない
メインゴルーチン(main)が終わると他のゴルーチンの終了を待たずにプログラム全体が終了する
後者ですが「sync.WaitGroup」を使用することで、待ち合わせすることができます。
package main
import (
"fmt"
"sync"
"time"
)
func say(s string) {
for i := 0; i < 2; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
say("world")
}()
say("hello")
wg.Wait()
}
[結果]
// 実行結果(1回目)
hello
world
world
hello
// 実行結果(2回目)
hello
world
world
hello
// 実行結果(3回目)
world
hello
hello
world
「world」が必ず出力されるようになりました。
チャネルについて
言語仕様を見てみましょう。さき(H.Saki)さんの記事もわかりやすかったため、合わせて記載します。
こちらも A Tour of Goのコードで確認してみましょう。
注3:デバッグのため「fmt.Println」を追加しています。
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
fmt.Println(len(s))
fmt.Println(s[:len(s)/2])
fmt.Println(s[len(s)/2:])
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
ソース:https://go.dev/tour/concurrency/2
[結果]
// 配列の要素数
6
// 配列の中身(先頭〜中央)
[7 2 8]
// 配列の中身(中央〜後尾)
[-9 4 0]
// チャネルで受け取った値、合計値
-5 17 12
int 配列の数値を、別々のゴルーチン(先頭〜中央、中央〜後尾)で合算し、チャネルを使用して合算値をメインゴルーチンで受け取っています。異なるゴルーチン同士で、値の送受信ができていますね。
弊社での活用事例
お待たせしました!
それでは弊社での活用事例を紹介します。弊社では、Circuit Xの一斉メール送信機能で活用しています。
Circuit Xのプロダクトについては下記をご覧下さい。
概要図
説明
EC2(ダッシュボード)で一斉メール送信
メール情報(送信者、本文など)をRDSに実行待ちキューとして保存
EC2からECS(メール送信バッチ)へ起動要求
ECS(メール送信バッチ)がAmazon SES経由でメールを送信 ←★
ここ(★)でゴルーチン(sync.WaitGroup)・チャネルを使い、並行処理でメールを送信しています。
当時の課題
移行前はダッシュボード(Rails)から直接メールを送信していた
メディア数の増加に伴いAppサーバ(Unicorn)のタイムアウトがたびたび発生
一斉メール送信機能が使用できない状態へ
ECS(Fargate)+ Go の選定理由
SendGrid などのSaaSサービスもあるが、ダッシュボード(Rails)で使用していたAmazon SESを活用する
データ量が増えても容易にスケールアップ、スケールアウトができるようにする
レガシーシステムからの脱却する中で知見がたまってきた
Goを使用してみたい
アーキテクチャ検討会の様子
CTOの若杉さんと検討しました(2019年10月下旬)
最後に
一斉メール送信機能は、私が入社して3ヶ月目に担当しました。(関連するダッシュボードの改修はもう一人のメンバが担当してくれました。)
当時、ECSもGoもわからない私でしたが、先輩方がサポートして下さったおかげでなんとかリリースすることができました。
フクロウラボには、Goはもちろん、新しいことにチャレンジできる風土、切磋琢磨して成長できる環境があります。
まだまだ弱々Gopherですが、世界のGopherの皆さまと開発ができるよう、引き続き、自己研磨していきたいと思います。
Enjoy development !
この記事が気に入ったらサポートをしてみませんか?