繁体   English   中英

如何编写异步LINQ查询?

[英]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日)“脆弱”:

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

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

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

  1. 您无法在IQueryable的最终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-to-SQL,这是比较耗时的:-P。 从高质量的,开源的LINQ-to-SQL提供程序实现开始,将是一个不错的选择。 您需要重新实现它的原因是,这样您就可以访问所有用于实现DbDataReader结果的列映射信息,并将其备份回对象实例,而不会丢失任何信息。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM