xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

TPL入門 (5) - スレッドローカル変数

今回はスレッドローカル変数を利用したパフォーマンス向上について見ていきます。

スレッドローカル変数の必要性

例えば、とある数値コレクションの値を合計したいとします。通常のfor/foreach文のループを用いて合計する場合は、何も気にすることなくひとつの変数に対して和して行くでしょう。しかし、複数のスレッドから同一の変数に対してアクセスする場合は、同時に書き込まれることを防ぐため "排他処理" をする必要があります。排他処理をすることで問題になるのは、「いくら複数のスレッドを立てても和する瞬間は結局並列じゃない」ということです。こうなった場合、もともとの処理を並列化するオーバーヘッドと、共通の変数にアクセスするための排他処理を行うオーバーヘッドが積み重なり、折角並列化しても結局速度を上げることができないという事態に陥ります。逆に言うと、これらのオーバーヘッドさえ極力防ぐことができれば、並列処理の恩恵を享受できる可能性が高まるということです。それを解決するためにTPLで導入されているのが "スレッドローカル変数" という考え方です。

スレッドローカル変数とは

スレッドローカル変数について理解するには、TPLがどのように要素毎の処理を並列化しているのかを知る必要があります。次の手抜きなイメージ図を見てください。

ThreadLocalVariables

実行までのフローは次のような感じです。

  1. Parallel.For/ForEachメソッドを実行
  2. コレクションを仮想的に分割する "パーティション" ができる
  3. 必要に応じて最適な数のスレッドが生成される
  4. パーティション単位でコレクション要素がスレッドに割り振られて処理される

各パーティションが生成された段階で、そのパーティション単位でのみ利用できる唯一の変数 (= スレッドローカル変数) を生成します。ひとつのパーティションがコレクション要素に対して行う処理は並列ではありません。ですので、同一のパーティション内での処理ならば排他制御をかけることなく、その変数にアクセスすることができます。このようにして排他制御に対するオーバーヘッドを小さくしています。

また、スレッドはひとつのコレクション要素を処理するたびに生成/破棄されるわけではないということです。処理実行時に必要に応じて適切な数のスレッドを生成し、同じスレッドを使い回しながら処理を実行していきます。これにより並列化を行うこと自体のオーバーヘッドを減らしています。スレッドの使い回しは、.NET Frameworkに従来から搭載されているスレッドプール (ThreadPool : 生成したスレッドをキャッシュして効率的に利用するための仕組み) の機構に依るものです。TPLでは、至る所で内部的にThreadPoolが使われています。

注意

パーティションが本当に上記のようなものであるかは確信がありません。MSDNに次のような記述があるため、そうではないかと推測しています。

ForEachループが実行されると、そのソースコレクションが複数のパーティションに分割されます。各パーティションには、"スレッドローカル" 変数が個別にコピーされます ("スレッドローカル" という用語は少し不正確です。1つのスレッド上で2つのパーティションが実行されることがあるからです)。

サンプル

次に、スレッドローカル変数を用いた簡単なサンプルを示します。また、Parallel.ForEachメソッドの各引数に対する説明も以下の通りです。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace Sample07_ThreadLocalVariables
{
    class Program
    {
        static void Main()
        {
            int total = 0;
            Parallel.ForEach
            (
                Enumerable.Range(1, 100),
                () => 0,
                (value, state, local) => local += value,
                local => Interlocked.Add(ref total, local)
            );
            Console.WriteLine(total);
        }
    }
}
 
//----- 結果 : 5050
引数 説明
第1引数 並列処理を行うコレクションソースを指定します。ここでは、1 ~ 100までの整数を生成します。
第2引数 スレッドローカル変数の初期化して返却します。もちろん、独自型を返すことも可能です。
第3引数 各コレクション要素に対する処理を行います。変数localには実行中のパーティションに属するスレッドローカル変数が渡ります。ここでは、パーティションから流れてきた値を和しています。
第4引数 各パーティションの処理がすべて終了したときの終了処理を行います。引数にはパーティションに属するスレッドローカル変数が渡ってきます。ここでは、共通の変数に対して排他制御をかけながら和しています。

このようにすることで、コレクション要素すべてに対して毎回スレッド/パーティションの生成/破棄を行うことはなくなります。また共通変数へのアクセスも、生成されたパーティション分しか排他制御をかける必要がありません。TPLはプログラマに対して最小の手間で最大のパフォーマンスを出せるように配慮してくれています。

次回予告

今回はスレッドローカル変数を用いた高速化について見てきました。次回は、並列処理中に発生した例外の扱い方について見ていきます。