[英]How to write Asynchronous LINQ query?
閱讀了一堆與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(無類型args),並使用反射來訪問字段(ICK !!)。 另一種方法是將回調聲明為“動態” ...哦...等等...還沒有結束。 :)這是如何使用動態的另一個不錯的例子。 有些人可能稱其為虐待。
將此扔到您的實用程序庫中:
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;
}
}
}
現在將其放在我的博客上,非常方便。
TheSoftwareJedi和ulrikb (aka user316318)解決方案適用於任何LINQ類型,但是(如Chris Moschini所指出的)不委托給利用Windows I / O完成端口的底層異步調用。
Wesley Bakker的Asynchronous DataContext帖子(由Scott Hanselman的博客帖子觸發)描述了LINQ to SQL的類,該類使用sqlCommand.BeginExecuteReader / sqlCommand.EndExecuteReader,該類利用Windows I / O完成端口。
I / O完成端口提供了一種有效的線程模型,用於在多處理器系統上處理多個異步I / O請求。
基於Michael Freidgeim的回答和Scott Hansellman提到的博客文章,以及可以使用async
/ await
事實,您可以實現可重用的ExecuteAsync<T>(...)
方法,該方法異步執行底層SqlCommand
:
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
。 GetCommand()
獲得的抽象DbCommand
實例進行GetCommand()
以獲取SqlCommand
。 如果您使用的是SQL CE,那么您會很不走運,因為SqlCeCommand
不會公開BeginExecuteReader
和EndExecuteReader
的異步模式。 SqlCommand
使用BeginExecuteReader
和EndExecuteReader
,可以在傳遞給BeginExecuteReader
方法的完成回調委托中獲得DbDataReader
。 DbDataReader
,它不知道它包含哪些列,也不知道如何將這些值映射回IQueryable
的ElementType
(對於聯接,很可能是匿名類型)。 當然,這時您可以手寫自己的列映射器,將其結果具體化為您的匿名類型或其他類型。 您必須為每種查詢結果類型編寫一個新的查詢類型,具體取決於LINQ-to-SQL如何對待您的IQueryable及其生成的SQL代碼。 這是一個很討厭的選擇,我不建議這樣做,因為它無法維護,也不總是正確的。 LINQ-to-SQL可以根據您傳入的參數值更改查詢形式,例如query.Take(10).Skip(0)
生成與query.Take(10).Skip(10)
不同的SQL,也許不同的結果集架構。 最好的選擇是以編程方式解決此實現問題: IQueryable
的ElementType
類型的LINQ到SQL映射屬性按定義的順序從DbDataReader
中拉出列。 正確實施此方法可能是此解決方案中最具挑戰性的部分。 正如其他人發現的那樣, 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數據訪問層中引起問題。
這些問題很容易克服-根本不要在查詢中執行這些問題,因為這兩種模式都不會對查詢的最終結果產生任何好處。 希望此建議適用於所有可能導致對象實現問題的查詢模式:-P。 解決無法訪問LINQ-to-SQL的列映射信息是一個難題。
解決問題的更“完整”的方法是有效地重新實現幾乎所有的LINQ-to-SQL,這是比較耗時的:-P。 從高質量的,開源的LINQ-to-SQL提供程序實現開始,將是一個不錯的選擇。 您需要重新實現它的原因是,這樣您就可以訪問所有用於實現DbDataReader
結果的列映射信息,並將其備份回對象實例,而不會丟失任何信息。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.