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 に合わせるのがよい。

関連

出典