0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goで学ぶ並行処理パターン:Fan-out/Fan-inパターン

Posted at

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パターンを活用して、より効果的な並行処理設計を行ってみてください!

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?