59 votes

Comment écrire une requête LINQ asynchrone ?

Après avoir lu beaucoup de choses sur LINQ, j'ai soudain réalisé qu'aucun article n'introduit comment écrire une requête LINQ asynchrone.

Supposons que nous utilisions LINQ to SQL, l'énoncé ci-dessous est clair. 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 actuelle des requêtes LINQ ne propose pas de prise en charge à cet égard.

Existe-t-il un moyen de programmer de manière asynchrone avec LINQ? Cela fonctionne comme s'il y avait une notification de rappel lorsque les résultats sont prêts à être utilisés sans aucun retard bloquant sur l'E/S.

36voto

TheSoftwareJedi Points 15921

Alors que LINQ n'a pas vraiment cela en soi, le framework lui-même le fait... Vous pouvez facilement créer votre propre exécuteur de requête asynchrone en environ 30 lignes... En fait, je viens de le composer 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 locaux. Ainsi, vous n'avez aucun moyen de définir votre fonction de rappel. C'est une chose assez importante car beaucoup de trucs 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 celui-ci est le 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 sorti. :) C'est un autre exemple de comment dynamic pourrait être utilisé. Certains pourraient appeler cela de l'abus.

Insérez ceci dans votre bibliothèque d'utilitaires :

public static class AsynchronousQueryExecutor
{
    public static void Call(IEnumerable query, Action> callback, Action errorCallback)
    {
        Func, IEnumerable> func =
            new Func, IEnumerable>(InnerEnumerate);
        IEnumerable result = null;
        IAsyncResult ar = func.BeginInvoke(
                            query,
                            new AsyncCallback(delegate(IAsyncResult arr)
                            {
                                try
                                {
                                    result = ((Func, IEnumerable>)((AsyncResult)arr).AsyncDelegate).EndInvoke(arr);
                                }
                                catch (Exception ex)
                                {
                                    if (errorCallback != null)
                                    {
                                        errorCallback(ex);
                                    }
                                    return;
                                }
                                //les erreurs à l'intérieur d'ici sont le problème du rappel
                                //Je pense que ce serait confus de les signaler
                                callback(result);
                            }),
                            null);
    }
    private static IEnumerable InnerEnumerate(IEnumerable query)
    {
        foreach (var item in query) //le méthode bloque ici pendant l'exécution de la requête
        {
            yield return item;
        }
    }
}

Et vous pourriez l'utiliser comme ceci :

class Program
{

    public static void Main(string[] args)
    {
        //cela pourrait être votre requête linq
        var qry = TestSlowLoadingEnumerable();

        //Nous lançons l'appel et lui donnons notre délégué de rappel
        //et un délégué vers un gestionnaire d'erreur
        AsynchronousQueryExecutor.Call(qry, HandleResults, HandleError);

        Console.WriteLine("L'appel a commencé sur un thread séparé, l'exécution s'est poursuivie");
        Console.ReadLine();
    }

    public static void HandleResults(IEnumerable results)
    {
        //les résultats sont disponibles ici
        foreach (var item in results)
        {
            Console.WriteLine(item);
        }
    }

    public static void HandleError(Exception ex)
    {
        Console.WriteLine("erreur");
    }

    //juste une énumération d'exemple à chargement lent
    public static IEnumerable TestSlowLoadingEnumerable()
    {
        Thread.Sleep(5000);
        foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 })
        {
            yield return i;
        }
    }

}

Je vais maintenant le mettre sur mon blog, assez pratique.

16voto

Michael Freidgeim Points 4002

Les solutions de TheSoftwareJedi et ulrikb(aka user316318) sont bonnes pour tout type de LINQ, mais (comme souligné par Chris Moschini) ne délèguent PAS aux appels asynchrones sous-jacents qui utilisent les ports de complétion d'E/S de Windows.

L'article de Wesley Bakker sur le DataContext asynchrone (déclenché par un billet de blog de Scott Hanselman) décrit une classe pour LINQ to SQL qui utilise sqlCommand.BeginExecuteReader/sqlCommand.EndExecuteReader, ce qui tire parti des ports de complétion d'E/S de Windows.

Les ports de complétion d'E/S offrent un modèle de file d'attente efficace pour le traitement de multiples requêtes E/S asynchrones sur un système multiprocesseur.

7voto

Nenad Points 3779

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

protected static async Task> ExecuteAsync(IQueryable 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(reader);
}

Et vous pouvez ensuite l'utiliser de cette maniè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);
        }
    }
}

4voto

James Dunne Points 1602

J'ai lancé un projet github simple nommé Asynq pour exécuter des requêtes LINQ-to-SQL de manière asynchrone. L'idée est assez simple même si elle est actuellement "fragile" (au 16/08/2011):

  1. Laissez LINQ-to-SQL faire le "travail lourd" de traduire votre IQueryable en une DbCommand via DataContext.GetCommand().
  2. Pour SQL 200[058], procédez à une conversion à partir de l'instance abstraite de DbCommand obtenue avec GetCommand() pour obtenir un SqlCommand. Si vous utilisez SQL CE, vous êtes de malchance puisque SqlCeCommand ne 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 standard du framework .NET pour obtenir un DbDataReader dans le délégué de rappel de complétion que vous passez à la méthode BeginExecuteReader.
  4. Maintenant, nous avons un DbDataReader dont nous ne connaissons pas les colonnes contenues ni comment mapper ces valeurs avec l'élément ElementType de l'IQueryable (souvent un type anonyme en cas de jointure). Bien sûr, à ce stade, vous pourriez écrire votre propre mappage de colonnes qui matérialise ses résultats dans votre type anonyme ou autre. Vous devriez en écrire un nouveau pour chaque type de résultat de requête, en fonction de la manière dont LINQ-to-SQL traite votre IQueryable et du code SQL qu'il génère. C'est une option assez mauvaise et je ne le recommande pas car ce n'est ni maintenable ni toujours correct. LINQ-to-SQL peut modifier la forme de votre requête en fonction des valeurs de paramètres que vous passez, par exemple query.Take(10).Skip(0) produit un SQL différent de query.Take(10).Skip(10), et peut-être un schéma de jeu de résultats différent. Votre meilleure option est de gérer ce problème de matérialisation de manière programmative:
  5. "Ré-implémenter" un matérialiseur d'objets en temps d'exécution simpliste qui extrait les colonnes du DbDataReader dans un ordre défini selon les attributs de mappage LINQ-to-SQL du type ElementType pour l'IQueryable. Implémenter cela 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 qu'un DbDataReader directement à un objet proxy LINQ-to-SQL correctement attribué. Étant donné que la plupart des requêtes intéressantes à écrire en LINQ vont impliquer des jointures complexes qui nécessitent inévitablement des types anonymes pour la clause finale select, il est assez inutile d'utiliser cette méthode édulcorée fournie par DataContext.Translate() de toute façon.

Il y a quelques inconvénients mineurs à cette solution lorsqu'on exploite le fournisseur IQueryable LINQ-to-SQL existant et mature:

  1. Vous ne pouvez pas mapper une seule instance d'objet sur plusieurs propriétés de types anonymes dans la clause select finale de votre IQueryable, par exemple from x in db.Table1 select new { a = x, b = x }. LINQ-to-SQL garde une trace en interne des associations des ordinaux de colonnes avec les propriétés; il n'expose pas ces informations à l'utilisateur final donc vous ne savez pas quelles colonnes du DbDataReader sont réutilisées et lesquelles sont "distinctes".
  2. Vous ne pouvez pas inclure de valeurs constantes dans votre clause select finale - celles-ci ne sont pas traduites en SQL et seront absentes du DbDataReader, vous devriez donc construire une logique personnalisée pour remonter ces valeurs constantes à partir de l'arbre Expression de l'IQueryable, ce qui serait assez compliqué et n'est tout simplement pas justifiable.

Je suis sûr qu'il existe d'autres motifs de requête qui pourraient poser problème mais ce sont les deux plus importants auxquels j'ai pensé qui pourraient causer des problèmes dans une couche d'accès de données LINQ-to-SQL existante.

Il est facile de résoudre ces problèmes - il suffit de ne pas les utiliser dans vos requêtes, étant donné que aucun des deux motifs n'apporte de bénéfice au résultat final de la requête. Espérons que ce conseil s'applique à tous les motifs de requête potentiellement susceptibles de causer des problèmes de matérialisation d'objets :-P. C'est un problème difficile à résoudre sans accès aux informations de mappage de colonnes de LINQ-to-SQL.

Une approche plus "complète" pour résoudre le problème serait de réimplémenter presque tout de LINQ-to-SQL, ce qui est un peu plus chronophage :-P. Partir d'une implémentation de qualité, open-source du fournisseur LINQ-to-SQL serait une bonne option ici. La raison pour laquelle vous auriez besoin de le réimplémenter est pour avoir accès à toutes les informations de mappage de colonnes utilisées pour matérialiser les résultats du DbDataReader vers une instance objet sans perte d'informations.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X