Advent LINQ (9): ParallelQuery対応拡張メソッドの実装

LINQクエリ中で使用出来る拡張メソッドを実装するとき、その引数シグネチャはIEnumerable<T>で受ける事になる。

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

この拡張メソッドを使ってみる。

// mscorlib.dllのすべての型から、生成可能なクラスを抽出する
var types = typeof(object).Assembly.GetTypes();
var creatables = types.OfCreatable();

この時、typesはType[]なので、引数のIEnumerable<Type>に合致する。OfCreatableに実装したLINQクエリは以下のように拡張メソッドで書き直せる。

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            enumerable.Where(type =>
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null));    // パブリックなデフォルトコンストラクタがある
    }
}

ここで、typesを並列化したらどうなるだろうか。

// mscorlibのすべての型から、生成可能なクラスを抽出する
var types = typeof(object).Assembly.GetTypes().AsParallel();    // 並列化
var creatables = types.OfCreatable();

varを書き直すと、以下のようになる。

// mscorlib.dllのすべての型から、生成可能なクラスを抽出する
ParallelQuery<Type> types = typeof(object).Assembly.GetTypes().AsParallel();  // 並列化
IEnumerable<Type> creatables = types.OfCreatable();

AsParallel()によって、列挙子の方がParallelQuery<Type>となる。しかし、OfCreatableの引数はIEnumerable<Type>なので、暗黙のキャストが発生し、結局OfCreatable内のLINQクエリは並列化されないまま実行される(以前の連載で述べたように、これは暗黙のゲートだ)。念のため、こういうことだ:

// mscorlib.dllのすべての型から、生成可能なクラスを抽出する
ParallelQuery<Type> types = typeof(object).Assembly.GetTypes().AsParallel();  // 並列化
IEnumerable<Type> creatables = LinqExtensions.OfCreatable((IEnumerable<Type>)types);    // IEnumerable<Type>に戻してから渡される

では、OfCreatable内のLINQクエリを並列化したい場合はどうすれば良いだろうか?

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable.AsParallel()    // <-- ここで並列化
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

並列化したいのだから、内部のLINQクエリでもAsParallel()で並列化すればよいと思うかもしれないが、これは微妙だ。引数のところでゲートが出来ていることは解消されない(IEnumerable<Type>になる)ため、OfCreatableを呼び出す直前のクエリが並列クエリであったとしても、一旦並列性が解除されてしまう。
また、戻り値として返される列挙子もIEnumerable<Type>なので、ここでもゲートを作ってしまう。そして、AsParallelがハードコードされたことで、わざと非並列状態で実行させたくても出来ないという問題もある。
これらを解消するには、以下のようにすればよい。

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する(非並列化)
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }

    // パブリックなデフォルトコンストラクタを持つ型を抽出する(並列化)
    public static ParallelQuery<Type> OfCreatable(this ParallelQuery<Type> parallelEnumerable)
    {
        return
            from type in parallelEnumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

要するに、引数(と戻り値)の列挙子の型が、ParallelQuery<T>となるようなオーバーロードを用意する。すると、中のLINQクエリの記述が全く同一であったとしても、それらの拡張メソッド(WhereやSelect等)は、Enumerable.WhereとParallelEnumerable.Whereのように呼び分けが行われる。結果として、並列バージョンではクエリ全体が並列化され、IEnumerable<T>のような通常の列挙子から呼び出される場合は、非並列クエリとなる。
これで目的を達したのだが、最後にこのメソッドを、非並列バージョンと並列バージョンのクラスに分ける。

// LINQ拡張メソッド群(非並列バージョン)
public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}
// LINQ拡張メソッド群(並列バージョン)
public static class ParallelLinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static ParallelQuery<Type> OfCreatable(this ParallelQuery<Type> parallelEnumerable)
    {
        return
            from type in parallelEnumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

このように、非並列バージョンと並列バージョンの拡張メソッドを分けることで、丁度EnumerableクラスとParallelEnumerableクラスが分けられているのと同じ構造となる。

投稿者:

kekyo

A strawberry red slime mold. Likes metaprogramming. MA. Bicycle rider. http://amzn.to/1SeuUwD