web-dev-qa-db-fra.com

Comment écrire une requête LINQ asynchrone?

Après avoir lu un tas de choses liées à LINQ, je me suis soudain rendu compte qu'aucun article n'introduisait comment écrire une requête LINQ asynchrone.

Supposons que nous utilisons LINQ to SQL, la déclaration ci-dessous est claire. Cependant, si la base de données SQL répond lentement, alors le thread utilisant ce bloc de code serait entravé.

var result = from item in Products where item.Price > 3 select item.Name;
foreach (var name in result)
{
    Console.WriteLine(name);
}

Il semble que la spécification de requête LINQ actuelle ne prenne pas en charge cela.

Existe-t-il un moyen de faire une programmation asynchrone LINQ? Cela fonctionne comme s'il y avait une notification de rappel lorsque les résultats sont prêts à être utilisés sans aucun délai de blocage sur les E/S.

57
Morgan Cheng

Bien que LINQ n'ait pas vraiment cela en soi, le cadre lui-même le fait ... Vous pouvez facilement rouler votre propre exécuteur de requête asynchrone en 30 lignes environ ... En fait, je viens de jeter cela ensemble pour vous :)

EDIT: En écrivant ceci, j'ai découvert pourquoi ils ne l'ont pas implémenté. Il ne peut pas gérer les types anonymes car ils sont de portée locale. Ainsi, vous n'avez aucun moyen de définir votre fonction de rappel. Ceci est une chose assez importante car beaucoup de choses linq to sql les crée dans la clause select. Toutes les suggestions ci-dessous subissent le même sort, donc je pense toujours que celle-ci est la plus facile à utiliser!

EDIT: La seule solution est de ne pas utiliser de types anonymes. Vous pouvez déclarer le rappel comme prenant simplement IEnumerable (pas d'arguments de type) et utiliser la réflexion pour accéder aux champs (ICK !!). Une autre façon serait de déclarer le rappel comme "dynamique" ... oh ... attendez ... Ce n'est pas encore fini. :) Ceci est un autre exemple décent de la façon dont la dynamique pourrait être utilisée. Certains peuvent appeler cela de l'abus.

Jetez ceci dans votre bibliothèque d'utilitaires:

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

Et vous pouvez l'utiliser comme ceci:

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

}

Je vais mettre ça sur mon blog maintenant, assez pratique.

35
TheSoftwareJedi

Les solutions de TheSoftwareJedi et lrikb (aka user316318) sont bonnes pour tout type LINQ, mais (comme indiqué par Chris Moschini ) NE PAS déléguer aux appels asynchrones sous-jacents qui tirent parti de Windows I/O Ports d'achèvement.

Le message de Wesley Bakker Asynchronous DataContext (déclenché par n billet de blog de Scott Hanselman ) décrit la classe pour LINQ to SQL qui utilise sqlCommand.BeginExecuteReader/sqlCommand.EndExecuteReader, qui exploite Windows I/O Ports d'achèvement.

ports d'achèvement d'E/S fournissent un modèle de thread efficace pour le traitement de plusieurs demandes d'E/S asynchrones sur un système multiprocesseur.

15
Michael Freidgeim

Basé sur réponse de Michael Freidgeim et mentionné article de blog de Scott Hansellman et sur le fait que vous pouvez utiliser async/await, vous pouvez implémenter réutilisable ExecuteAsync<T>(...), qui exécute de manière asynchrone SqlCommand sous-jacente:

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

Et puis vous pouvez (ré) l'utiliser comme ceci:

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

J'ai démarré un projet github simple nommé Asynq pour faire une exécution de requête LINQ-to-SQL asynchrone. L'idée est assez simple mais "fragile" à ce stade (au 16/08/2011):

  1. Laissez LINQ-to-SQL effectuer le travail "lourd" de traduction de votre IQueryable en DbCommand via le DataContext.GetCommand().
  2. Pour SQL 200 [058], convertissez à partir de l'instance abstraite DbCommand que vous avez obtenue de GetCommand() pour obtenir un SqlCommand. Si vous utilisez SQL CE, vous n'avez pas de chance car SqlCeCommand n'expose pas le modèle asynchrone pour BeginExecuteReader et EndExecuteReader.
  3. Utilisez BeginExecuteReader et EndExecuteReader sur le SqlCommand en utilisant le modèle d'E/S asynchrone du framework .NET standard pour obtenir un DbDataReader dans le délégué de rappel d'achèvement que vous passez au BeginExecuteReader méthode.
  4. Maintenant, nous avons un DbDataReader dont nous n'avons aucune idée de quelles colonnes il contient ni comment mapper ces valeurs sur le IQueryable's ElementType (le plus susceptible d'être un type anonyme dans le cas de jointures). Bien sûr, à ce stade, vous pouvez écrire à la main votre propre mappeur de colonnes qui matérialise ses résultats dans votre type anonyme ou autre. Vous devez en écrire un nouveau pour chaque type de résultat de requête, selon la façon dont LINQ-to-SQL traite votre IQueryable et le code SQL qu'il génère. C'est une option assez désagréable et je ne la recommande pas car elle n'est pas maintenable et ne serait pas toujours correcte. LINQ-to-SQL peut modifier votre formulaire de requête en fonction des valeurs de paramètre que vous transmettez, par exemple query.Take(10).Skip(0) produit un SQL différent de query.Take(10).Skip(10), et peut-être un schéma d'ensemble de résultats différent. Votre meilleur pari est de gérer ce problème de matérialisation par programme:
  5. "Ré-implémenter" un matérialiseur d'objet d'exécution simpliste qui extrait les colonnes de DbDataReader dans un ordre défini en fonction des attributs de mappage LINQ-to-SQL de ElementType Type pour IQueryable. L'implémenter correctement est probablement la partie la plus difficile de cette solution.

Comme d'autres l'ont découvert, la méthode DataContext.Translate() ne gère pas les types anonymes et ne peut mapper un DbDataReader directement à un objet proxy LINQ-to-SQL correctement attribué. Étant donné que la plupart des requêtes qui méritent d'être écrites dans LINQ vont impliquer des jointures complexes qui finissent inévitablement par nécessiter des types anonymes pour la clause de sélection finale, il est assez inutile d'utiliser cette méthode édulcorée DataContext.Translate() fournie de toute façon.

Il y a quelques inconvénients mineurs à cette solution lors de l'exploitation du fournisseur IQueryable LINQ-to-SQL existant:

  1. Vous ne pouvez pas mapper une seule instance d'objet à plusieurs propriétés de type anonyme dans la clause de sélection finale de votre IQueryable, par exemple from x in db.Table1 select new { a = x, b = x }. LINQ-to-SQL garde en interne le suivi des ordonnances de colonne vers quelles propriétés; il n'expose pas ces informations à l'utilisateur final, vous n'avez donc aucune idée des colonnes de DbDataReader qui sont réutilisées et qui sont "distinctes".
  2. Vous ne pouvez pas inclure de valeurs constantes dans votre clause de sélection finale - celles-ci ne sont pas traduites en SQL et seront absentes du DbDataReader vous devez donc créer une logique personnalisée pour extraire ces valeurs constantes du IQueryable _ Expression arbre, ce qui serait assez compliqué et n'est tout simplement pas justifiable.

Je suis sûr qu'il existe d'autres modèles de requête qui pourraient se casser, mais ce sont les deux plus importants auxquels je pourrais penser qui pourraient causer des problèmes dans une couche d'accès aux données LINQ-to-SQL existante.

Ces problèmes sont faciles à vaincre - ne les faites tout simplement pas dans vos requêtes car aucun des schémas n'apporte aucun avantage au résultat final de la requête. Espérons que ce conseil s'applique à tous les modèles de requête qui pourraient potentiellement causer des problèmes de matérialisation d'objet :-P. Il est difficile de résoudre le problème de ne pas avoir accès aux informations de mappage de colonne de LINQ-to-SQL.

Une approche plus "complète" pour résoudre le problème serait de réimplémenter efficacement presque tout LINQ-to-SQL, ce qui prend un peu plus de temps :-P. Partir d'une implémentation de fournisseur LINQ-to-SQL open-source de qualité serait une bonne façon d'aller ici. La raison pour laquelle vous auriez besoin de la réimplémenter est pour que vous ayez accès à toutes les informations de mappage de colonne utilisées pour matérialiser les résultats DbDataReader sur une instance d'objet sans aucune perte d'informations.

4
James Dunne