Как написать асинхронный запрос 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? Он работает так, как будто есть обратный вызов уведомление, когда результаты готовы к использованию без какой-либо задержки блокировки ввода-вывода.

Ответы

Ответ 1

В то время как LINQ на самом деле не имеет этого, сама фреймворка... Вы можете легко свернуть собственный асинхронный исполнитель запросов в 30 строк или так... На самом деле, я просто бросил это вместе для вас:)

РЕДАКТИРОВАТЬ: Благодаря написанию этого, я обнаружил, почему они не реализовали его. Он не может обрабатывать анонимные типы, поскольку они локализованы локально. Таким образом, у вас нет способа определить вашу функцию обратного вызова. Это довольно важная вещь, поскольку многие элементы linq to sql создают их в предложении select. Любое из приведенных ниже предложений страдает той же судьбой, поэтому я по-прежнему считаю, что этот самый простой в использовании!

EDIT: Единственное решение - не использовать анонимные типы. Вы можете объявить обратный вызов, просто принимая IEnumerable (без аргументов типа), и используйте отражение для доступа к полям (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;
        }
    }

}

Идти, чтобы поместить это в мой блог сейчас, довольно удобно.

Ответ 2

TheSoftwareJedi и ulrikb (aka user316318) решения хороши для любого типа LINQ, но (как указано Chris Moschini) НЕ передают базовые асинхронные вызовы, которые используют порты завершения ввода-вывода Windows.

Wesley Bakker Асинхронный DataContext post (вызванный сообщение в блоге Scott Hanselman) описывают класс для LINQ to SQL, который использует sqlCommand.BeginExecuteReader/sqlCommand.EndExecuteReader, которые используют порты завершения ввода-вывода Windows.

Порт завершения ввода/вывода обеспечивает эффективную модель потоков для обработки нескольких асинхронных запросов ввода-вывода в многопроцессорной системе.

Ответ 3

На основе Майкл Фрейдзим ответил и упомянул сообщение в блоге от 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);
}

И тогда вы можете (re) использовать его следующим образом:

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);
        }
    }
}

Ответ 4

Я начал простой проект github Asynq для выполнения асинхронного выполнения запросов LINQ-to-SQL. Идея довольно простая, хотя и "хрупкая" на этом этапе (по состоянию на 8/16/2011):

  • Пусть LINQ-to-SQL выполняет "тяжелую" работу по переводу вашего IQueryable в DbCommand через DataContext.GetCommand().
  • Для SQL 200 [058], отбрасывается из абстрактного экземпляра DbCommand, который вы получили от GetCommand(), чтобы получить SqlCommand. Если вы используете SQL CE, вам не повезло, поскольку SqlCeCommand не раскрывает шаблон async для BeginExecuteReader и EndExecuteReader.
  • Используйте BeginExecuteReader и EndExecuteReader с помощью SqlCommand, используя стандартный асинхронный шаблон ввода-вывода .NET framework, чтобы получить DbDataReader в делегате завершения обратного вызова, который вы передаете методу BeginExecuteReader.
  • Теперь у нас есть DbDataReader, который мы не знаем, какие столбцы он содержит, и как сопоставить эти значения с IQueryable ElementType (скорее всего, это анонимный тип в случае объединения). Несомненно, на этом этапе вы можете вручную написать собственный редактор столбцов, который материализует свои результаты обратно в ваш анонимный тип или что-то еще. Вам нужно будет написать новый по каждому типу результата запроса, в зависимости от того, как LINQ-to-SQL обрабатывает ваш IQueryable и какой код SQL он генерирует. Это довольно неприятный вариант, и я не рекомендую его, так как он не поддерживается и не всегда будет правильным. LINQ-to-SQL может изменить форму запроса в зависимости от значений параметров, которые вы передаете, например, query.Take(10).Skip(0) создает другой SQL, чем query.Take(10).Skip(10), и, возможно, другую схему набора результатов. Лучше всего решить эту проблему материализации:
  • "Повторно реализовать" упрощенный материализатор объектов времени выполнения, который вытягивает столбцы из DbDataReader в определенном порядке в соответствии с атрибутами отображения LINQ-to-SQL типа ElementType для IQueryable. Реализация этого правильно, вероятно, является наиболее сложной частью этого решения.

Как выяснили другие, метод DataContext.Translate() не обрабатывает анонимные типы и может отображать только DbDataReader непосредственно к объекту прокси-объекта LINQ-to-SQL. Поскольку большинство запросов, которые стоит написать в LINQ, будут связаны с сложными объединениями, которые неизбежно в конечном итоге требуют анонимных типов для окончательного предложения select, довольно бессмысленно использовать этот предоставленный метод DataContext.Translate() с опущенным слоем.

Есть несколько незначительных недостатков в этом решении при использовании существующего поставщика услуг IQueryable с именем LINQ to SQL:

  • Вы не можете сопоставить экземпляр одного объекта с несколькими свойствами анонимного типа в заключительном предложении select вашего IQueryable, например. from x in db.Table1 select new { a = x, b = x }. LINQ-to-SQL внутренне отслеживает, какие координаты столбцов сопоставляются с какими свойствами; он не предоставляет эту информацию конечному пользователю, поэтому вы не знаете, какие столбцы в DbDataReader повторно используются и которые являются "отличными".
  • Вы не можете включать постоянные значения в свое окончательное предложение select - они не могут быть переведены в SQL и будут отсутствовать в DbDataReader, поэтому вам нужно будет построить пользовательскую логику, чтобы вывести эти постоянные значения из IQueryable Дерево Expression, что было бы довольно хлопотно и просто не оправдано.

Я уверен, что есть другие шаблоны запросов, которые могут сломаться, но это два самых больших, о которых я мог подумать, что может вызвать проблемы на существующем уровне доступа к данным LINQ-to-SQL.

Эти проблемы легко победить - просто не делайте их в своих запросах, так как ни один шаблон не дает никакой пользы от конечного результата запроса. Надеюсь, этот совет применим ко всем шаблонам запросов, которые потенциально могут вызвать проблемы материализации материализации: -P. Это трудная задача решить, не имея доступа к информации сопоставления столбцов LINQ-to-SQL.

Более "полный" подход к решению проблемы будет состоять в том, чтобы эффективно перепрофилировать почти все LINQ-to-SQL, что является немного более трудоемким: -P. Исходя из качества, реализация провайдера LINQ-to-SQL с открытым исходным кодом была бы хорошим выбором. Причина, по которой вам нужно будет переопределить его, - это доступ ко всей информации сопоставления столбцов, используемой для материализации результатов DbDataReader, обратно к экземпляру объекта без потери информации.