如何编写异步LINQ查询?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何编写异步LINQ查询?相关的知识,希望对你有一定的参考价值。

在我读了一堆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到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(又名user316318)解决方案适用于任何LINQ类型,但(如Chris Moschini所指出的)不委托利用Windows I / O完成端口的底层异步调用。

Wesley Bakker的Asynchronous DataContext帖子(由a blog post of Scott Hanselman触发)描述LINQ to SQL的类,它使用sqlCommand.BeginExecuteReader / sqlCommand.EndExecuteReader,它利用Windows I / O完成端口。

I/O completion ports提供了一种有效的线程模型,用于在多处理器系统上处理多个异步I / O请求。

另一答案

基于Michael Freidgeim's answer并提到blog post from 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日):

  1. 让LINQ-to-SQL通过IQueryable完成将DbCommand翻译成DataContext.GetCommand()的“繁重”工作。
  2. 对于SQL 200 [058],从你从DbCommand获得的抽象GetCommand()实例中获得一个SqlCommand。如果你使用的是SQL CE,那你就不幸了,因为SqlCeCommand没有公开BeginExecuteReaderEndExecuteReader的异步模式。
  3. 使用标准的.NET框架异步I / O模式在BeginExecuteReader上使用EndExecuteReaderSqlCommand来获得你传递给DbDataReader方法的完成回调委托中的BeginExecuteReader
  4. 现在我们有一个DbDataReader,我们不知道它包含哪些列,也不知道如何将这些值映射回IQueryableElementType(在连接的情况下最有可能是匿名类型)。当然,此时您可以手工编写自己的列映射器,将其结果具体化为您的匿名类型或其他类型。您必须为每个查询结果类型编写一个新的,具体取决于LINQ-to-SQL如何处理您的IQueryable以及它生成的SQL代码。这是一个非常讨厌的选项,我不推荐它,因为它不可维护,也不总是正确的。 LINQ-to-SQL可以根据传入的参数值更改查询形式,例如query.Take(10).Skip(0)生成的不同于query.Take(10).Skip(10)的SQL,也可能是不同的结果集模式。您最好的选择是以编程方式处理此实现问题:
  5. “重新实现”一个简单的运行时对象物理化器,它根据DbDataReaderElementType类型的LINQ-to-SQL映射属性,以定义的顺序从IQueryable中拉出列。正确实现这可能是此解决方案中最具挑战性的部分。

正如其他人发现的那样,DataContext.Translate()方法不处理匿名类型,只能将DbDataReader直接映射到正确归属的LINQ-to-SQL代理对象。由于大多数值得在LINQ中编写的查询都涉及复杂的连接,这最终不可避免地需要最终select子句的匿名类型,所以无论如何使用这个提供的watered-down DataContext.Translate()方法都是毫无意义的。

在利用现有成熟的LINQ-to-SQL IQueryable提供程序时,此解决方案存在一些小缺点:

  1. 您不能在IQueryable的final select子句中将单个对象实例映射到多个匿名类型属性,例如from x in db.Table1 select new { a = x, b = x }。 LINQ-to-SQL在内部跟踪哪些列序列映射到哪些属性;它不会将此信息公开给最终用户,因此您不知道DbDataReader中哪些列被重用以及哪些列是“不同的”。
  2. 您不能在最终的select子句中包含常量值 - 这些不会被转换为SQL并且将不会出现在DbDataReader中,因此您必须构建自定义逻辑以从IQueryableExpression树中提取这些常量值,这将是相当的麻烦,根本就没有道理。

我确信还有其他查询模式可能会中断,但这些是我能想到的两个最大可能导致现有LINQ-to-SQL数据访问层出现问题的模式。

这些问题很容易被打败 - 只是不要在查询中执行它们,因为这两种模式都不会为查询的最终结果带来任何好处。希望这个建议适用于所有可能导致对象实现问题的查询模式:-P。解决不能访问LINQ-to-SQL的列映射信息是一个难题。

解决问题的一种更“完整

以上是关于如何编写异步LINQ查询?的主要内容,如果未能解决你的问题,请参考以下文章

C# linq 如何编写子查询?

如何使用实体框架和 linq 编写此 sql 查询

如何编写此 LINQ 查询的 SQL 版本?

如何在 Linq 中编写嵌套的 Sql 查询 [关闭]

如何使用 LINQ 异步调用带有子记录的数据库?

如何在Linq方法中更改MYSQL连接查询