Go言語では、goroutineやchannelといった標準機能により、シンプルかつパワフルな並行処理の実装が可能です。しかし、実務的な開発では、これらの基本要素を適切に組み合わせて可読性・保守性の高いコードを書くことが求められます。その中でよく使われる並行処理の「設計パターン」の一つにFan-out/Fan-inパターンがあります。
本記事では、Fan-out/Fan-inパターンを中心に、どのような場面で有効か、また実装例を示しながら解説します。
Fan-out/Fan-inパターンとは
Fan-outは、1つの入力ストリームや入力イベントに対して、「複数のgoroutineで並行に処理を広げる」ことを指します。例えば、ある仕事キューからタスクを読み取り、それらを複数のワーカー(goroutine)に振り分けて同時並行で処理する場面がFan-outパターンです。
例:
ユーザーからのリクエストを受け取り、その処理を複数のバックエンドサービスに並行問い合わせする。
複数のファイルを同時にパース・解析する。
CPUコア数に応じて複数のgoroutineで計算タスクを並列実行する。
Fan-inとは?
Fan-inは、複数の並行処理(goroutine)からの結果を一つのチャンネルなどに集約するパターンです。Fan-outで拡散された処理が終わったら、そのすべての結果をまとめ上げ、後続の処理に渡せます。これにより、並行処理後に一括して結果を処理したり、最終的な集約ステップを行うことが可能になります。
例:
複数の外部API問い合わせ結果を全て受け取り、レスポンスを統合してクライアントに返す。
並行処理されたデータ解析結果をまとめて集計する。
つまり、Fan-out/Fan-inパターンは「入力(1点) → 並行処理(多点) → 集約(1点)」という一連の流れを上手にモデル化することで、コードを整然とした並行パイプラインに仕上げる手法です。
Fan-out/Fan-inパターンの利点
並行処理の分業化:
タスクを複数のgoroutineに分配することで、I/O待機やCPU計算を並行に行い、スループットが向上します。
スケール性・柔軟性:
処理負荷が増えた場合、ワーカーgoroutineを増やすことで対処しやすいです。
コードの明確化:
入力ステップ(Fan-out)、処理ステップ(ワーカー群)、集約ステップ(Fan-in)が明確に分離されるため、並行フローが理解しやすくなります。
Fan-out/Fan-inパターンの実装例
以下に、簡易的な実装例を示します。ここでは以下の処理をモデルとします。
入力となる一連のタスク(整数値のスライス)を受け取り、それぞれについて「擬似的な重い計算」を並行に行うワーカーを複数稼働させ、結果を最終的に集約する。
package main
import (
"fmt"
"math/rand"
"time"
)
// 擬似的な重い処理
func heavyWork(n int) int {
// ランダムな時間待機しているふり
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
return n * n
}
func main() {
rand.Seed(time.Now().UnixNano())
inputs := []int{1, 2, 3, 4, 5}
// Fan-out: inputsをワーカー達に振り分ける
numWorkers := 3
inCh := make(chan int)
outCh := make(chan int)
// ワーカー起動
for i := 0; i < numWorkers; i++ {
go func() {
for n := range inCh {
result := heavyWork(n)
outCh <- result
}
}()
}
// 入力をinChに投入(Fan-outの開始)
go func() {
for _, n := range inputs {
inCh <- n
}
close(inCh)
}()
// Fan-in: 複数ワーカーからの結果を集約
results := make([]int, 0, len(inputs))
for i := 0; i < len(inputs); i++ {
res := <-outCh
results = append(results, res)
}
// 全ての結果が集まったので、ここから集約・処理を行う
fmt.Println("Results:", results)
}
コードの解説
入力と出力チャネル:
inChはワーカーへ仕事を渡すためのチャネル、outChはワーカーからの結果を受け取るためのチャネルです。
ワーカー群の起動 (Fan-out):
for i := 0; i < numWorkers; i++ { ... }で複数のgoroutineを起動し、それぞれがinChから仕事を受け取り処理結果をoutChに送るという単純なロジックを持っています。
入力をinChへ供給:
go func()で別goroutineを起動し、inputsをinChへ順次投入し終わった後にclose(inCh)しています。これによりワーカー側はinChのクローズを感知すると処理終了できます。
出力を受け取り集約 (Fan-in):
メインgoroutineはlen(inputs)回outChから受信し、すべてのワーカー処理が完了するのを待ちます。この部分がFan-in処理にあたります。
この設計により、ワーカー数や処理負荷を簡単に調整できます。また、処理の流れが明確なので並行処理ロジックが理解しやすく、保守性も向上します。
その他の並行処理パターン
Pipelineパターン:
処理を段階的に分け、複数の段階をチャネルでつなぐことで、データストリームをパイプラインのように流す手法。各ステージをgoroutineで並行処理できるため、段階的に非同期処理を組み上げやすい。
Worker Poolパターン:
Fan-out/Fan-inパターンの一部とも言えるが、一定数のワーカーgoroutineをプールして仕事を割り当てるパターン。スレッドプール的な概念で、リソースの効率的な活用を狙う。
Selectパターン:
複数のチャネルを同時に待ち受けて、到着したデータに応じて処理を行うselect文を活用する。非同期I/Oや複数のイベントを待機して処理を分岐する際に便利。
これらのパターンは、goroutineやchannelの基本に立脚しており、組み合わせることでさらに柔軟でスケーラブルな並行処理設計が可能になります。
まとめ
Fan-out/Fan-inパターンは、一つの入力を複数goroutineで並行処理し(Fan-out)、結果を一箇所に集約する(Fan-in)設計パターンです。
このパターンを使うことで、スループット向上や柔軟なスケールアウト、コードの見通しの良さを実現できます。
PipelineやWorker Poolなど、Goの並行処理では他にも多くのパターンが存在しますが、すべてはgoroutineとchannelという基本要素の理解から始まります。
Fan-out/Fan-inパターンを活用して、より効果的な並行処理設計を行ってみてください!