Est-il possible «d’attendre le retour de rendement DoSomethingAsync ()»

108

Les blocs d'itérateur réguliers (c'est-à-dire "yield return") sont-ils incompatibles avec "async" et "await"?

Cela donne une bonne idée de ce que j'essaie de faire:

async Task<IEnumerable<Foo>> Method(String [] Strs)
{
    // I want to compose the single result to the final result, so I use the SelectMany
    var finalResult = UrlStrings.SelectMany(link =>   //i have an Urlstring Collection 
                   await UrlString.DownLoadHtmlAsync()  //download single result; DownLoadHtmlAsync method will Download the url's html code 
              );
     return finalResult;
}

Cependant, j'obtiens une erreur du compilateur citant "impossible de charger la chaîne de message à partir des ressources".

Voici une autre tentative:

async Task<IEnumerable<Foo>> Method(String [] Strs)
{
    foreach(var str in strs)
    {
        yield return await DoSomethingAsync( str)
    }
}

Mais encore une fois, le compilateur renvoie une erreur: "impossible de charger la chaîne de message à partir des ressources".


Voici le vrai code de programmation de mon projet

Ceci est très utile lorsque j'ai une tâche de liste, cette tâche peut être télécharger du HTML à partir d'une URL et j'utilise la syntaxe "yield return wait task", le résultat est que je veux IEnumerable<Foo>. Je ne veux pas écrire ce code:

async Task<IEnumerable<String>> DownLoadAllURL(String [] Strs)
{
    List<Foo> htmls= new ...
    foreach(var str in strs)
    {
        var html= await DownLoadHtmlAsync( str)
        htmls.Add(item)
    }
    return htmls;
}

Mais il semble que je dois le faire.

Merci pour toute aide.

Jiangzhen
la source
4
Cette question n'est pas particulièrement claire, vous devez préciser ce que vous souhaitez faire exactement - il est difficile de déterminer ce que vous souhaitez faire à partir d'un simple échantillon de code.
Justin le
3
Le package NuGet Ix_Experimental-Async inclut des «énumérateurs asynchrones» avec prise en charge LINQ. Ils utilisent le IAsyncEnumerator<T>type défini par Arne.
Stephen Cleary
@StephenCleary ce package a été supprimé. Étant donné que cela remonte à très longtemps et qu'il a été créé par rxteam de MS, savez-vous si cela a été intégré à RX?
JoeBrockhaus
@JoeBrockhaus: On dirait que ça continue comme Ix-Async .
Stephen Cleary

Réponses:

72

Ce que vous décrivez peut être accompli avec la Task.WhenAllméthode. Remarquez comment le code se transforme en une simple ligne unique. Ce qui se passe, c'est que chaque URL individuelle commence le téléchargement, puis WhenAllest utilisée pour combiner ces opérations en une seule Taskqui peut être attendue.

Task<IEnumerable<string>> DownLoadAllUrls(string[] urls)
{
    return Task.WhenAll(from url in urls select DownloadHtmlAsync(url));
}
Brian Gideon
la source
10
async / await ne sont pas nécessaires dans ce cas. Retirez simplement asyncde la méthode et faites return Task.WhenAlldirectement.
luiscubal
22
La dernière ligne peut être écrite plus succinctement commeurls.Select(DownloadHtmlAsync)
BlueRaja - Danny Pflughoeft
1
Cela s'est avéré être le problème exact auquel j'étais confronté (télécharger paresseusement des chaînes à partir d'une série d'URI). Je vous remercie.
James Ko
13
Cela ne répond pas réellement à la question de Is it possible to await yield? vous venez de trouver une solution pour ce cas d'utilisation. N'a pas répondu à la question générale.
Buh Buh le
1
Je serais enclin à revenir Task<string[]>car cela indiquerait que vous ne retournez plus un itérateur avec une exécution différée, c'est-à-dire. toutes les URL sont en cours de téléchargement.
johnnycardy
73

tl; dr Les itérateurs implémentés avec yield sont une construction bloquante, donc à partir de maintenant, wait et yield sont incompatibles.

Long Puisque l'itération sur un IEnumerableest une opération bloquante, appeler une méthode marquée comme asyncl'exécutera toujours de manière bloquante, car elle doit attendre que l'opération se termine.

async Task<IEnumerable<Foo>> Method(String [] Strs)
{
  foreach(var str in strs)
  {
    yield return await DoSomethingAsync( str)
  }
}  

L'attente Methodmélange des significations. Voulez-vous attendre que le Taskait un IEnumerable, puis bloquer son itération? Ou essayez-vous d'attendre chaque valeur du IEnumerable?

Je suppose que le second est le comportement souhaité et dans ce cas, la sémantique Iterator existante ne fonctionnera pas. L' IEnumerator<T>interface est essentiellement

public interface IEnumerator<T>
  T Current;
  bool MoveNext();
}

J'ignore Reset()car cela n'a aucun sens pour une séquence de résultats asynchrones. Mais ce dont vous auriez besoin est quelque chose comme ceci:

public interface IAsyncEnumerator<T>
  T Current;
  Task<bool> MoveNext();
}

Bien sûr, foreachcela ne fonctionnera pas non plus et vous devrez itérer manuellement comme ceci:

var moveNext = await asyncEnumerator.MoveNext();
while(moveNext) {

  // get the value that was fetche asynchronously
  var v = asyncEnumerator.Current;

  // do something with that value

  // suspend current execution context until next value arrives or we are done
  moveNext = await asyncEnumerator.MoveNext();
}
Arne Claassen
la source
1
Pensez-vous qu'il est possible que la prochaine version du langage C # ajoute la foreachprise en charge de votre hypothétique IAsyncEnumerator<T>?
Dai
1
@Dai Je pense que c'est dans le pipeline. pour C # 8.0 / J'avais lu à ce sujet.
nawfal
40

Selon les nouvelles fonctionnalités de C # 8.0 ( lien # 1 et lien # 2 ), nous aurons un IAsyncEnumerable<T>support d'interface qui permettra d'implémenter votre deuxième tentative. Il ressemblera à ceci:

async Task<Foo> DoSomethingAsync(string url)
{
    ...
}       
// producing IAsyncEnumerable<T>
async IAsyncEnumerable<Foo> DownLoadAllURL(string[] strs)
{
    foreach (string url in strs)
    {
        yield return await DoSomethingAsync(url);
    }
}
...
// using
await foreach (Foo foo in DownLoadAllURL(new string[] { "url1", "url2" }))
{
    Use(foo);
}

Nous pouvons obtenir le même comportement en C # 5 mais avec une sémantique différente:

async Task<Foo> DoSomethingAsync(string url)
{
    ...
}
IEnumerable<Task<Foo>> DownLoadAllURL(string[] strs)
{
    foreach (string url in strs)
    {
        yield return DoSomethingAsync(url);
    }
}

// using
foreach (Task<Foo> task in DownLoadAllURL(new string[] { "url1", "url2" }))
{
    Foo foo = await task;
    Use(foo);
}

La réponse de Brian Gideon implique que le code appelant obtiendra de manière asynchrone une collection de résultats obtenus en parallèle. Le code ci-dessus implique que le code appelant obtiendra des résultats comme à partir d'un flux un par un de manière asynchrone.

AlbertK
la source
17

Je sais que je suis trop tard avec la réponse, mais voici une autre solution simple qui peut être obtenue avec cette bibliothèque:
GitHub: https://github.com/tyrotoxin/AsyncEnumerable
NuGet.org: https: //www.nuget .org / packages / AsyncEnumerator /
C'est beaucoup plus simple que Rx.

using System.Collections.Async;

static IAsyncEnumerable<string> ProduceItems(string[] urls)
{
  return new AsyncEnumerable<string>(async yield => {
    foreach (var url in urls) {
      var html = await UrlString.DownLoadHtmlAsync(url);
      await yield.ReturnAsync(html);
    }
  });
}

static async Task ConsumeItemsAsync(string[] urls)
{
  await ProduceItems(urls).ForEachAsync(async html => {
    await Console.Out.WriteLineAsync(html);
  });
}
Serge Semenov
la source
5

Cette fonctionnalité sera disponible à partir de C # 8.0. https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

Depuis MSDN

Flux asynchrones

La fonctionnalité async / await de C # 5.0 vous permet de consommer (et de produire) des résultats asynchrones dans un code simple, sans callback:

async Task<int> GetBigResultAsync()
{
    var result = await GetResultAsync();
    if (result > 20) return result; 
    else return -1;
}

Ce n'est pas si utile si vous souhaitez consommer (ou produire) des flux continus de résultats, tels que vous pourriez obtenir d'un appareil IoT ou d'un service cloud. Les flux asynchrones sont là pour cela.

Nous introduisons IAsyncEnumerable, qui est exactement ce à quoi vous vous attendez; une version asynchrone de IEnumerable. Le langage vous permet d'attendre chacun d'eux pour consommer leurs éléments, et leur rapporter pour produire des éléments.

async IAsyncEnumerable<int> GetBigResultsAsync()
{
    await foreach (var result in GetResultsAsync())
    {
        if (result > 20) yield return result; 
    }
}
Petter Pettersson
la source
1

Tout d'abord, gardez à l'esprit que le truc Async n'est pas terminé. L'équipe C # a encore un long chemin à parcourir avant la sortie de C # 5.

Cela étant dit, je pense que vous voudrez peut-être rassembler les tâches qui sont déclenchées dans la DownloadAllHtmlfonction d'une manière différente.

Par exemple, vous pouvez utiliser quelque chose comme ceci:

IEnumerable<Task<string>> DownloadAllUrl(string[] urls)
{
    foreach(var url in urls)
    {
        yield return DownloadHtmlAsync(url);
    }
}

async Task<string> DownloadHtmlAsync(url)
{
    // Do your downloading here...
}

Non pas que la DownloadAllUrlfonction ne soit PAS un appel asynchrone. Mais, vous pouvez avoir l'appel async implémenté sur une autre fonction (ie DownloadHtmlAsync).

La bibliothèque parallèle de tâches a les fonctions .ContinueWhenAnyet .ContinueWhenAll.

Cela peut être utilisé comme ceci:

var tasks = DownloadAllUrl(...);
var tasksArray = tasks.ToArray();
var continuation = Task.Factory.ContinueWhenAll(tasksArray, completedTasks =>
{
    completedtask
});
continuation.RunSynchronously();
John Gietzen
la source
Vous pouvez également avoir un 'en-tête' (avant la boucle) si vous définissez votre méthode comme asynchrone Task<IEnumerable<Task<string>>> DownloadAllUrl. Ou, si vous voulez des actions de «pied de page» IEnumerable<Task>. Eg gist.github.com/1184435
Jonathan Dickinson
1
Maintenant que asynctout est terminé et que C # 5.0 est publié, cela peut être mis à jour.
casperOne
J'aime celui-ci, mais comment dans ce cas foreach (var url dans await GetUrls ()) {yield return GetContent (url)}; J'ai seulement trouvé un moyen de diviser en deux méthodes.
Juan Pablo Garcia Coello
-1

Cette solution fonctionne comme prévu. Notez la await Task.Run(() => enumerator.MoveNext())partie.

using (var enumerator = myEnumerable.GetEnumerator())
{
    while (true)
    {
        if (enumerator.Current != null)
        {
            //TODO: do something with enumerator.Current
        }

        var enumeratorClone = monitorsEnumerator;
        var hasNext = await Task.Run(() => enumeratorClone.MoveNext());
        if (!hasNext)
        {
            break;
        }
    }
}
François
la source