Как написать асинхронный запрос LINQ? - PullRequest
57 голосов
/ 31 октября 2008

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

Ответы [ 4 ]

35 голосов
/ 31 октября 2008

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

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

РЕДАКТИРОВАТЬ: Единственное решение состоит в том, чтобы не использовать анонимные типы. Вы можете объявить обратный вызов как просто принимающий 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;
        }
    }

}

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

15 голосов
/ 03 июля 2011

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

пост Асинхронного DataContext Уэсли Баккера (инициированный публикацией в блоге Скотта Хансельмана ) описывает класс для LINQ to SQL, использующий sqlCommand.BeginExecuteReader / sqlCommand.EndExecuteReader, который использует Windows I / O Порты завершения.

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

5 голосов
/ 31 июля 2016

На основании ответа Михаила Фрейдгейма и упомянутого сообщения в блоге Скотта Хансельмана и того факта, что вы можете использовать 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);
        }
    }
}
4 голосов
/ 17 августа 2011

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

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...