多数のLINQ関連のものを読んだ後、非同期LINQクエリの作成方法を紹介する記事がないことに突然気付きました。
LINQ to SQLを使用するとします。以下のステートメントは明確です。ただし、SQLデータベースの応答が遅い場合、このコードブロックを使用するスレッドが妨げられます。
var result = from item in Products where item.Price > 3 select item.Name;
foreach (var name in result)
{
Console.WriteLine(name);
}
現在のLINQクエリ仕様はこれをサポートしていないようです。
非同期プログラミングLINQを実行する方法はありますか? I/Oのブロッキング遅延なしに結果を使用する準備ができたときにコールバック通知があるように機能します。
LINQには実際にはこれはありませんが、フレームワーク自体にはあります...独自の非同期クエリエグゼキュータを30行程度で簡単に実行できます。実際、これを一緒に投げました:)
編集:これを書くことで、なぜ実装されなかったのかを発見しました。匿名型はローカルスコープであるため、匿名型を処理できません。したがって、コールバック関数を定義する方法はありません。これは多くのlinq to sqlがselect句でそれらを作成するため、かなり重要なことです。以下の提案はいずれも同じ運命をたどるので、私はまだこれが最も使いやすいと思います!
編集:唯一の解決策は、匿名型を使用しないことです。 IEnumerable(引数なし)を取得するだけでコールバックを宣言し、リフレクションを使用してフィールドにアクセスできます(ICK !!)。別の方法は、コールバックを「動的」として宣言することです...ああ...待って...それはまだ出ていません。 :)これは、動的の使用方法のもう1つの適切な例です。乱用と呼ぶ人もいます。
ユーティリティライブラリでこれをスローします。
public static class AsynchronousQueryExecutor
{
public static void Call<T>(IEnumerable<T> query, Action<IEnumerable<T>> callback, Action<Exception> errorCallback)
{
Func<IEnumerable<T>, IEnumerable<T>> func =
new Func<IEnumerable<T>, IEnumerable<T>>(InnerEnumerate<T>);
IEnumerable<T> result = null;
IAsyncResult ar = func.BeginInvoke(
query,
new AsyncCallback(delegate(IAsyncResult arr)
{
try
{
result = ((Func<IEnumerable<T>, IEnumerable<T>>)((AsyncResult)arr).AsyncDelegate).EndInvoke(arr);
}
catch (Exception ex)
{
if (errorCallback != null)
{
errorCallback(ex);
}
return;
}
//errors from inside here are the callbacks problem
//I think it would be confusing to report them
callback(result);
}),
null);
}
private static IEnumerable<T> InnerEnumerate<T>(IEnumerable<T> query)
{
foreach (var item in query) //the method hangs here while the query executes
{
yield return item;
}
}
}
そして、次のように使用できます。
class Program
{
public static void Main(string[] args)
{
//this could be your linq query
var qry = TestSlowLoadingEnumerable();
//We begin the call and give it our callback delegate
//and a delegate to an error handler
AsynchronousQueryExecutor.Call(qry, HandleResults, HandleError);
Console.WriteLine("Call began on seperate thread, execution continued");
Console.ReadLine();
}
public static void HandleResults(IEnumerable<int> results)
{
//the results are available in here
foreach (var item in results)
{
Console.WriteLine(item);
}
}
public static void HandleError(Exception ex)
{
Console.WriteLine("error");
}
//just a sample lazy loading enumerable
public static IEnumerable<int> TestSlowLoadingEnumerable()
{
Thread.Sleep(5000);
foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 })
{
yield return i;
}
}
}
これをブログに載せて、とても便利です。
SoftwareJediと lrikb の(別名user316318)ソリューションはどのLINQタイプにも適していますが、( Chris Moschini が示すように)Windows Iを活用する基礎となる非同期呼び出しに委任しません。/O完了ポート。
Wesley Bakkerの Asynchronous DataContext post( Scott Hanselmanのブログ投稿 によってトリガーされます)は、Windows I/O完了ポート。
I/O完了ポート マルチプロセッサシステムで複数の非同期I/O要求を処理するための効率的なスレッドモデルを提供します。
Michael Freidgeim's answer と言及 Scott Hansellmanのブログ投稿 と、async
/await
を使用できるという事実に基づいて、再利用可能を実装できます基になるSqlCommand
を非同期に実行するExecuteAsync<T>(...)
メソッド:
protected static async Task<IEnumerable<T>> ExecuteAsync<T>(IQueryable<T> query,
DataContext ctx,
CancellationToken token = default(CancellationToken))
{
var cmd = (SqlCommand)ctx.GetCommand(query);
if (cmd.Connection.State == ConnectionState.Closed)
await cmd.Connection.OpenAsync(token);
var reader = await cmd.ExecuteReaderAsync(token);
return ctx.Translate<T>(reader);
}
そして、次のように(再)使用できます:
public async Task WriteNamesToConsoleAsync(string connectionString, CancellationToken token = default(CancellationToken))
{
using (var ctx = new DataContext(connectionString))
{
var query = from item in Products where item.Price > 3 select item.Name;
var result = await ExecuteAsync(query, ctx, token);
foreach (var name in result)
{
Console.WriteLine(name);
}
}
}
Asynq という名前の単純なgithubプロジェクトを開始して、非同期のLINQ-to-SQLクエリ実行を行いました。この段階では「壊れやすい」とはいえ、アイデアは非常に単純です(2011年8月16日時点)。
DataContext.GetCommand()
を介してIQueryable
をDbCommand
に変換する「重い」作業をさせます。DbCommand
を取得するには、GetCommand()
から取得した抽象SqlCommand
インスタンスからキャストします。 SQL CEを使用している場合、SqlCeCommand
はBeginExecuteReader
とEndExecuteReader
の非同期パターンを公開しないため、運が悪いです。BeginExecuteReader
からEndExecuteReader
とSqlCommand
を使用して、完了コールバックデリゲートでDbDataReader
を取得します。 BeginExecuteReader
メソッドに渡します。DbDataReader
ができました。これにはどの列が含まれているかわからず、それらの値をIQueryable
のElementType
(ほとんどの場合匿名型である可能性が高い)結合の場合)。もちろん、この時点で、匿名の型などに結果を具体化する独自の列マッパーを手書きで書くことができます。 LINQ-to-SQLがIQueryableを処理する方法と生成するSQLコードに応じて、クエリ結果タイプごとに新しいものを作成する必要があります。これはかなり厄介なオプションであり、保守可能ではなく、常に正しいとは限らないため、お勧めしません。 LINQ-to-SQLは、渡すパラメーター値に応じてクエリフォームを変更できます。たとえば、query.Take(10).Skip(0)
はquery.Take(10).Skip(10)
とは異なるSQLを生成し、おそらく異なる結果セットスキーマを生成します。あなたの最善の策は、この実体化問題をプログラムで処理することです。DbDataReader
のElementType
タイプのLINQ-to-SQLマッピング属性に従って、定義された順序でIQueryable
から列を引き出す単純なランタイムオブジェクトマテリアライザーを「再実装」します。 ] _。これを正しく実装することは、おそらくこのソリューションの最も難しい部分です。他の人が発見したように、DataContext.Translate()
メソッドは匿名型を処理せず、DbDataReader
を適切に属性付けされたLINQ-to-SQLプロキシオブジェクトに直接マップすることしかできません。 LINQで記述する価値のあるクエリのほとんどは、最終的なselect句に匿名型を必要とする複雑な結合を必要とするため、この提供されたDataContext.Translate()
メソッドをとにかく使用するのはかなり無意味です。
既存の成熟したLINQ-to-SQL IQueryableプロバイダーを活用する場合、このソリューションにはいくつかの小さな欠点があります。
IQueryable
の最後のselect句で、単一のオブジェクトインスタンスを複数の匿名型プロパティにマッピングすることはできません。 from x in db.Table1 select new { a = x, b = x }
。 LINQ-to-SQLは、どの列序数がどのプロパティにマップされているかを内部的に追跡します。この情報はエンドユーザーに公開されないため、DbDataReader
のどの列が再利用され、どの列が「異なる」かはわかりません。DbDataReader
に存在しないため、IQueryable
のExpression
ツリー。これは非常に面倒で、単に正当化することはできません。壊れる可能性のあるクエリパターンは他にもあるはずですが、これらは既存のLINQ-to-SQLデータアクセスレイヤーで問題を引き起こす可能性があると考えられる2つの最大のパターンです。
これらの問題は簡単に解決できます。どちらのパターンもクエリの最終結果にメリットをもたらさないため、クエリでは単純に実行しないでください。このアドバイスが、オブジェクトの実体化の問題を引き起こす可能性のあるすべてのクエリパターンに適用されることを願っています。 LINQ-to-SQLの列マッピング情報にアクセスできないことを解決するのは難しい問題です。
問題を解決するためのより「完全な」アプローチは、LINQ-to-SQLのほぼすべてを効果的に再実装することです。これはもう少し時間がかかります:-P。品質から始めると、オープンソースのLINQ-to-SQLプロバイダー実装がここに行く良い方法です。再実装する必要があるのは、DbDataReader
の結果を具体化するために使用されるすべての列マッピング情報にアクセスして、情報を失うことなくオブジェクトインスタンスに戻すためです。