TL;DR
一定数の worker(スレッドやプロセスなど)を維持し、タスクを効率よく並列に実行するデザインパターン。Thread Pool pattern、Replicated workers pattern、Master-Worker pattern、Worker-crew pattern とも呼ばれる。
なぜ必要か
大量のタスクを並列に処理したいが、使うリソースは制限したい場合に役立つ。worker のプールを事前に作って再利用するので、worker を作る負荷も減らせる。
仕組み
タスクはキュー(チャネルやメッセージキューなど)に溜まる。システムは、あらかじめ設定した固定数の worker を起動する。実装によっては、負荷に応じて worker 数の上限を一時的に増やせる。
各 worker はキューからタスクを取り出して処理し、終わったら次のタスクを取り出す。キューが空になると、新しいタスクが来るまで待機状態になる。システムの設計によってはアイドル状態になることもある。
例
Go では、ゴルーチンを worker とし、チャネルをキューにして実装できる。
func read(r io.Reader) (int, error) {
var count int64
var n = 10
ch := make(chan []byte, n)
wg := sync.WaitGroup{}
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
for b := range ch {
v := task(b)
atomic.AddInt64(&count, int64(v))
}
}()
}
for {
b := make([]byte, 1024)
_, err := r.Read(b)
if err != nil {
if err == io.EOF {
break
}
return 0, nil
}
ch <- b
}
close(ch)
wg.Wait()
return int(count), nil
}ほかの実装例は Go by Example: ワーカープール にある。
注意点
Go のゴルーチンはとても軽量なので、worker を再利用する恩恵はあまり受けられない。Go でプールを使うなら、プールの大きさは GOMAXPROCS に合わせるのがよい。