RSS

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

09 12月

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クラスが分けられているのと同じ構造となる。

広告
 
コメントする

投稿者: : 2013/12/09 投稿先 .NET, LINQ

 

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

 
%d人のブロガーが「いいね」をつけました。