落ち穂拾いの第2回の今回はスレッドプールの挙動について見ていきます。TPLは既定の設定のほぼすべてで内部的にスレッドプールが利用されています。つまり、スレッドプールはTPLの要と言っても過言ではありません。スレッドプールの内部実装は.NET Framework 4のリリースに合わせて大幅に改良されたとのことです。そのスレッドプールの内部動作が、いかによく出来ているかを感じていただければと思います。
アニメーションで知る内部挙動
CLRのスレッドプールにはグローバルキューがあります。また、グローバルキューに投入されたタスクを実行するワーカースレッドがいます。各ワーカースレッドは個別にローカルキューを持っているのが特徴です。ワーカースレッドは必要に応じて動的に複数生成され、不必要になったら破棄されます。
説明
スライドの動作を順番に説明すると次のような感じです。実によくできています。
- UIスレッドから並列処理すべきタスクがグローバルキューに投入される。
- スレッドプールにワーカースレッドがない場合、ワーカースレッドが生成される。
- ワーカースレッド1がグローバルキューからタスクを取得する。 (※1)
- ワーカースレッド2も同様にグローバルキューからタスクを取得する。
- ワーカースレッド1、2がタスクを消化する。このとき、ワーカースレッド1から子タスクが生成される。 (※2)
- ワーカースレッド1はローカルキュー1からタスクを取得する。それと同時にワーカースレッド2はグローバルキューからタスクを取得する。 (※3)
- それぞれタスクを消化する。
- ワーカースレッド1はローカルキュー1からタスクを取得する。それと同時にワーカースレッド2はローカルキュー1からタスクを奪い取る。 (※4)
- それぞれタスクを消化する。
- スレッドが破棄される。 (※5)
注釈 | 説明 |
---|---|
※1 | グローバルキューからのタスクの取得はFIFO (先入れ先出し方式) で行われます。処理すべき順に処理できるようにするためです。 |
※2 | ワーカースレッド上で生成された子タスク/入れ子タスクはローカルキューに投入されます。ワーカースレッドが率先して作業しなければならないものだからです。 |
※3 | 自身のローカルキューからのタスクの取得はLIFO (後入れ先出し方式) で行われます。このようにすることで、メモリ上のキャッシュを有効に活用でき、処理速度を大幅に向上させることができます。ただしその副作用として、投入したタスクが逆順に実行されます。ローカルキューの先頭にアクセスできるのは自身のワーカースレッドのみなので、同期ロックを取得する必要がなく、非常に高速に処理されます。 |
※4 | ワーカースレッドがタスクを取得する場合、まず自身のローカルキューを確認します。何もない場合はグローバルキューを探しに行きます。グローバルキューにもタスクがない場合、他ワーカースレッドのローカルキューの末尾からタスクを奪い取ります。これをワークスティーリングと言います。末尾から取ることで、同期ロックの取得を最小限に抑えています。ワークスティーリングの動作はそれほど頻繁に発生するものではないとのことですが、これこそ負荷分散の要となる動作であることは間違いありません。 |
※5 | ワーカースレッドが一定期間何も処理をしなかった場合、そのスレッドは不要として破棄されます。このようにすることで、可能な限りリソースの消費を抑えるようにしています。ただし、一定期間がどれほどかは明文化されていません。 |
注意
今回の記事内容は、プログラミング .NET Framework 第3版とMSDNの記述を読んでの個人的な解釈です。間違っている可能性も十分にありますので、もしそのようなことがあればご連絡ください。