2
votes

LINQ Sélectionnez analogique pour la méthode asynchrone

Disons que j'ai l'exemple de code suivant:

0 before
1 before
2 before
2 after
0 after
1 after

La TestMethod fonctionnera jusqu'à la fin 3 fois, donc je verrai 3 paires de avant et after:

private static async Task<int> TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    await Task.Delay(50);
    Console.WriteLine($"{param} after");
    return param;
}

Maintenant, je dois rendre TestMethod asynchrone:

0 before
0 after
1 before
1 after
2 before
2 after

Comment puis-je J'écris une expression Select similaire pour cette méthode asynchrone? Si j'utilise juste un lambda asynchrone Enumerable.Range (0, 3) .Select (async x => wait TestMethod (x)). ToArray (); , cela ne fonctionnera pas car il a gagné ' t attendre la fin, donc avant les parties seront appelées en premier:

private static async Task Main(string[] args)
{
    var result = Enumerable.Range(0, 3).Select(x => TestMethod(x)).ToArray();
    Console.ReadKey();
}

private static int TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    Thread.Sleep(50);
    Console.WriteLine($"{param} after");
    return param;
}

Notez que je ne veux pas exécuter les 3 appels en parallèle - J'ai besoin qu'ils s'exécutent un par un, en commençant le suivant uniquement lorsque le précédent est complètement terminé et a renvoyé la valeur.


8 commentaires

Et si vous utilisiez simplement foreach ?


@JSteward est certainement une option, mais le style LINQ est juste beaucoup plus court et élégant. Je vais créer une méthode d'extension enveloppant foreach , j'espérais juste qu'il existe déjà une solution dans la bibliothèque standard.


@Aleksey Shubin Considérez-vous ma réponse comme correcte ou non?


Vous dites donc que d'autres exigences stipulent que TestMethod doit être async , mais dans ce cas particulier, vous voulez le maintenir en fonctionnement synchrone? Sinon, je ne vois aucune raison de rendre la méthode async . Si oui, vous pouvez renvoyer TestMethod (x) .Result .


@ArturMustafin non, dans votre réponse, vous bloquez le thread, perdant tous les avantages de l'exécution asynchrone. L'appel de TestMethod (x) .Result serait le même et plus facile.


@GertArnold Je veux l'exécuter de manière asynchrone mais pas en parallèle - des choses un peu différentes. Dans ma vraie tâche, je voulais faire plusieurs appels à DbContext.SaveChangesAsync Entity Framework qui ne permet pas une exécution parallèle, mais il y a toujours un avantage de l'appel asynchrone - il ne bloque pas le fil d'appel.


@AlekseyShubin "Notez que je ne veux pas exécuter les 3 appels en parallèle - j'ai besoin qu'ils s'exécutent un par un, en commençant le suivant uniquement lorsque le précédent est complètement terminé et a renvoyé la valeur."


@ArturMustafin Je ne vois pas votre point. Oui, je ne le souhaite pas en parallèle, mais je le souhaite toujours asynchrone - voir ce commentaire pour plus de détails. Votre réponse le rend synchrone.


6 Réponses :


-2
votes

Vous pouvez utiliser les primitives de synchronisation

    class Program
    {
        static async Task Main(string[] args)
        {
            var waitHandle = new AutoResetEvent(true);
            var result = Enumerable
                .Range(0, 3)
                .Select(async (int param) => {
                    waitHandle.WaitOne();
                    await TestMethod(param);
                    waitHandle.Set();
                }).ToArray();
            Console.ReadKey();
        }
        private static async Task<int> TestMethod(int param)
        {
            Console.WriteLine($"{param} before");
            await Task.Delay(50);
            Console.WriteLine($"{param} after");
            return param;
        }
    }


0 commentaires

4
votes

Je rencontre régulièrement cette exigence et je ne connais aucune solution intégrée pour y répondre comme en C # 7.2. Je reviens généralement à l'utilisation de await sur chaque opération asynchrone dans un foreach , mais vous pouvez choisir votre méthode d'extension:

static async Task Main(string[] args)
{
    var result = (await Enumerable.Range(0, 3).SelectAsync(x => TestMethod(x))).ToArray();
    Console.ReadKey();
}

Vous appelleriez alors wait sur le SelectAsync:

public static class EnumerableExtensions
{
    public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, Task<TResult>> asyncSelector)
    {
        var results = new List<TResult>();
        foreach (var item in source)
            results.Add(await asyncSelector(item));
        return results;
    }
}

L'inconvénient de cette approche est que le SelectAsync est impatient, pas paresseux. C # 8 promet d'introduire des flux asynchrones , ce qui permettra de redevenir paresseux.


1 commentaires

Belle solution! Le type de retour Task> pourrait cependant créer de fausses attentes d'évaluation paresseuse. Le renvoi de Task pourrait être plus approprié.



0
votes

Vous devez être conscient de ce qu'est un objet Enumerable. Un objet Enumerable n'est pas l'énumération elle-même. Cela vous donne la possibilité d'obtenir un objet Enumerator. Si vous avez un objet Enumerator, vous pouvez demander les premiers éléments d'une séquence, et une fois que vous avez un élément énuméré, vous pouvez demander le suivant.

Donc, créer un objet qui vous permet d'énumérer sur une séquence, n'a rien à voir avec l'énumération elle-même.

Votre fonction Select renvoie uniquement un objet Enumerable, elle ne démarre pas l'énumération. L'énumération est lancée par ToArray.

Au niveau le plus bas, l'énumération se fait comme suit:

 var result = Enumerable.Range(0, 3)
     .Select(i => TestMethod(i))
     .ToListAsync();

ToArray appellera en interne GetEnumerator et MoveNext . Donc, pour rendre votre instruction LINQ asynchrone, vous aurez besoin d'un ToArrayAsync.

Le code est assez simple. Je vais vous montrer ToListAsync comme une méthode d'extension, ce qui est un peu plus facile à faire.

static class EnumerableAsyncExtensions
{
    public static async Task<List<TSource>> ToListAsync<TSource>(
       this IEnumerable<Task<TSource>> source)
    {
        List<TSource> result = new List<TSource>();
        var enumerator = source.GetEnumerator()
        while (enumerator.MoveNext())
        {
            // in baby steps. Feel free to do this in one step
            Task<TSource> current = enumerator.Current;
            TSource awaitedCurrent = await current;
            result.Add(awaitedCurrent);
        }
        return result;
    } 
}

Vous n'aurez à créer cela qu'une seule fois. Vous pouvez l'utiliser pour n'importe quel ToListAsync où vous devrez attendre pour chaque élément:

IEnumerable<TSource> myEnumerable = ...
IEnumerator<TSource> enumerator = myEnumerable.GetEnumerator();
while (enumerator.MoveNext())
{
     // there is still an element in the enumeration
     TSource currentElement = enumerator.Current;
     Process(currentElement);
}

Notez que le retour de Select est IEnumerable > : un objet qui permet d'énumérer une séquence d'objets Task . Dans un foreach, à chaque cycle, vous obtenez une Task


4 commentaires

Je me demande ce que j'ai fait de mal pour mériter un vote défavorable. Si vous ne commentez pas lorsque vous votez contre, je ne peux pas améliorer ma réponse et je ne saurai jamais ce qui ne va pas


Je me demande aussi pourquoi quelqu'un a voté contre. Informations très intéressantes sur Enumerable , je ne m'en rendais pas compte. Je suis arrivé à une méthode d'extension ToListAsync très similaire, sauf que j'ai utilisé foreach au lieu de travailler directement avec Enumerable (voir la réponse que j'ai ajoutée). Je crois comprendre que foreach fait fondamentalement la même chose sous le capot. Est-ce correct ou est-ce que je rate quelque chose?


foreach appelle en interne GetEnumerator () / MoveNext () / Current . Il est donc généralement beaucoup plus facile d'utiliser foreach et yield return si vous souhaitez renvoyer un IEnumerable<...> au lieu d'une List <...> . Je n'utilise GetEnumerator et MoveNext que si l'énumération dépend de ce que j'ai lu. Un bon exemple est Any . Essayez de coder cela en utilisant un foreach et comparez-le avec le simple return enumerator.MoveNext ();


Cette solution est un peu dangereuse, car elle dépend du fait que la source IEnumerable> n'est pas matérialisée. L'appelant pourrait facilement faire l'erreur de matérialiser l'énumérable différé des tâches, en appelant ToList par exemple, et cela changerait complètement le comportement en démarrant toutes les tâches à la fois. Pour cette raison, je suggérerais d'ajouter une vérification que la source ne peut pas être convertie en collection matérialisée: if (source is ICollection >) throw new ArgumentException ();



3
votes

Dans C # 8 ( la prochaine version majeure au moment de la rédaction) il y aura un support pour IAsyncEnumrable où vous pouvez écrire async pour chaque boucle:

await foreach (var item in GetItemsAsync())
    // Do something to each item in sequence

J'attends là sera une méthode d'extension Select pour faire une projection, mais sinon, il n'est pas difficile d'écrire la vôtre. Vous pourrez également créer des blocs d'itérateur IAsyncEnumrable avec yield return .


0 commentaires

-2
votes

On dirait qu'il n'y a pas d'autre solution actuellement (jusqu'à C # 8.0) à part l'énumérer manuellement. J'ai créé une méthode d'extension pour cela (renvoie une liste au lieu d'un tableau car c'est plus simple):

public static async Task<List<T>> ToListAsync<T>(this IEnumerable<Task<T>> source)
{
    var result = new List<T>();
    foreach (var item in source)
    {
        result.Add(await item);
    }
    return result;
}


0 commentaires

1
votes

Je suppose que c'est ce que OP veut réellement, puisque j'ai passé une journée entière à résoudre la même question que se demandait OP. Mais je veux que cela se fasse en mehtod couramment.

Par la réponse de Martin Liversage, j'ai trouvé le bon moyen: IAsyncEnumerable et System.Linq.Async pacakge, qui sont disponible en C # 8.0, c'est ce que je veux.

Exemple:

private static async Task Main(string[] args)
{
    var result = await Enumerable.Range(0, 3)
        .ToAsyncEnumerable()
        .SelectAwait(x => TestMethod(x))
        .ToArrayAsync();
    Console.ReadKey();
}

private static async Task<int> TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    await Task.Delay(50);
    Console.WriteLine($"{param} after");
    return param;
}


4 commentaires

Agréable. Mais avez-vous oublié une attente? var result = wait Enumerable ...


Oui. Désolé pour ça.


Belle solution! Hélas, les énumérables asynchrones ne sont pris en charge que pour .NET Core, pas pour .NET Framework.


@AlekseyShubin, vous pouvez également utiliser des IAsyncEnumerable sur .NET Framework. Vous devez d'abord installer le package Microsoft.Bcl.AsyncInterfaces, puis ajouter la ligne 8 dans le fichier .csproj de votre projet. Voici les instructions détaillées.