È possibile "attendere il ritorno del rendimento DoSomethingAsync ()"


108

I blocchi iteratori regolari (cioè "yield return") sono incompatibili con "async" e "await"?

Questo dà una buona idea di quello che sto cercando di fare:

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

Tuttavia, ottengo un errore del compilatore che indica "impossibile caricare la stringa del messaggio dalle risorse".

Ecco un altro tentativo:

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

Ma ancora una volta, il compilatore restituisce un errore: "impossibile caricare la stringa del messaggio dalle risorse".


Ecco il vero codice di programmazione nel mio progetto

Questo è molto utile quando ho un'attività List, che può essere scaricata HTML da un URL e io uso la sintassi "yield return await task", il risultato è che voglio IEnumerable<Foo>. Non voglio scrivere questo codice:

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

Ma sembra che debba farlo.

Grazie per qualsiasi aiuto.


4
Questa domanda non è particolarmente chiara, dovresti elaborare esattamente ciò che desideri fare: è difficile determinare cosa desideri fare solo da un esempio di codice.
Justin

3
Il pacchetto NuGet Ix_Experimental-Async include "enumeratori asincroni" completi di supporto LINQ. Usano il IAsyncEnumerator<T>tipo definito da Arne.
Stephen Cleary

@StephenCleary quel pacchetto è stato rimosso. Dato che è stato molto tempo fa ed è stato scritto da rxteam di MS, sai se è stato inserito in RX?
JoeBrockhaus

@ JoeBrockhaus: sembra che viva come Ix-Async .
Stephen Cleary

Risposte:


72

Quello che stai descrivendo può essere realizzato con il Task.WhenAllmetodo. Notare come il codice si trasforma in un semplice one-liner. Quello che succede è che ogni singolo URL inizia a scaricarsi e quindi WhenAllviene utilizzato combinando queste operazioni in un'unica Taskche può essere attesa.

Task<IEnumerable<string>> DownLoadAllUrls(string[] urls)
{
    return Task.WhenAll(from url in urls select DownloadHtmlAsync(url));
}

10
async / await non sono necessari in questo caso. Basta rimuovere asyncdal metodo e fare return Task.WhenAlldirettamente.
luiscubal

22
L'ultima riga può essere scritta in modo più succinto comeurls.Select(DownloadHtmlAsync)
BlueRaja - Danny Pflughoeft

1
Questo si è rivelato essere il problema esatto che stavo affrontando (scaricando pigramente stringhe da una serie di URI). Grazie.
James Ko

13
Questo in realtà non risponde alla domanda: Is it possible to await yield? Hai appena trovato una soluzione per questo caso d'uso. Non ha risposto alla domanda generale.
Buh Buh

1
Sarei propenso a tornare in Task<string[]>quanto ciò indicherebbe che non stai più restituendo un iteratore con esecuzione differita, ad es. tutti gli URL vengono scaricati.
johnnycardy

73

tl; dr Gli iteratori implementati con yield sono un costrutto di blocco, quindi al momento wait e yield sono incompatibili.

Lungo Poiché l'iterazione su un IEnumerableè un'operazione di blocco, la chiamata di un metodo contrassegnato come asynclo eseguirà comunque in modo bloccante, poiché deve attendere che l'operazione termini.

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

L'attesa Methodmescola significati. Vuoi aspettare fino a quando Taskha un IEnumerablee poi bloccare l'iterazione su di esso? O stai cercando di attendere ogni valore del IEnumerable?

Presumo che il secondo sia il comportamento desiderato e in tal caso la semantica di Iterator esistente non funzionerà. L' IEnumerator<T>interfaccia è fondamentalmente

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

Lo sto ignorando Reset()poiché non ha senso per una sequenza di risultati asincroni. Ma ciò di cui avresti bisogno è qualcosa del genere:

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

Ovviamente, foreachanche questo non funzionerà e dovresti iterare manualmente in questo modo:

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

1
Pensi che sia possibile che la prossima versione in linguaggio C # aggiunga il foreachsupporto per il tuo ipotetico IAsyncEnumerator<T>?
Dai

1
@Dai, penso che sia molto in cantiere. per C # 8.0 / Avevo letto a riguardo.
nawfal


40

Secondo le nuove funzionalità in C # 8.0 ( collegamento # 1 e collegamento # 2 ) avremo il IAsyncEnumerable<T>supporto dell'interfaccia che consentirà di implementare il secondo tentativo. Sarà simile a questo:

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

Possiamo ottenere lo stesso comportamento in C # 5 ma con una semantica diversa:

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 risposta di Brian Gideon implica che il codice chiamante otterrà in modo asincrono una raccolta di risultati ottenuti in parallelo. Il codice sopra implica che il codice chiamante otterrà risultati come da uno stream uno per uno in modo asincrono.


17

So di essere in ritardo con la risposta, ma ecco un'altra semplice soluzione che può essere ottenuta con questa libreria:
GitHub: https://github.com/tyrotoxin/AsyncEnumerable
NuGet.org: https: //www.nuget .org / packages / AsyncEnumerator /
È molto più semplice di 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);
  });
}

5

Questa funzionalità sarà disponibile a partire da C # 8.0. https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

Da MSDN

Flussi asincroni

La funzionalità async / await di C # 5.0 consente di utilizzare (e produrre) risultati asincroni in codice semplice, senza callback:

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

Non è così utile se desideri consumare (o produrre) flussi continui di risultati, come potresti ottenere da un dispositivo IoT o da un servizio cloud. I flussi asincroni sono lì per questo.

Introduciamo IAsyncEnumerable, che è esattamente quello che ti aspetteresti; una versione asincrona di IEnumerable. Il linguaggio ti consente di attendere per ciascuno di essi per consumare i loro elementi e restituire loro il ritorno per produrre elementi.

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


1

Prima di tutto, tieni presente che le cose Async non sono finite. Il team di C # ha ancora molta strada da fare prima che venga rilasciato C # 5.

Detto questo, penso che potresti voler raccogliere i compiti che vengono licenziati nella DownloadAllHtmlfunzione in un modo diverso.

Ad esempio, puoi usare qualcosa del genere:

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 che la DownloadAllUrlfunzione NON sia una chiamata asincrona. Ma puoi avere la chiamata asincrona implementata su un'altra funzione (cioè DownloadHtmlAsync).

La libreria Task Parallel ha le funzioni .ContinueWhenAnye .ContinueWhenAll.

Può essere usato in questo modo:

var tasks = DownloadAllUrl(...);
var tasksArray = tasks.ToArray();
var continuation = Task.Factory.ContinueWhenAll(tasksArray, completedTasks =>
{
    completedtask
});
continuation.RunSynchronously();

Puoi anche avere un 'intestazione' (prima del ciclo) se definisci il tuo metodo come asincrono Task<IEnumerable<Task<string>>> DownloadAllUrl. Oppure, se desideri azioni "piè di pagina" IEnumerable<Task>. Ad esempio gist.github.com/1184435
Jonathan Dickinson

1
Ora che le asynccose sono finite e C # 5.0 è stato rilasciato, questo può essere aggiornato.
casperOne

Mi piace questo, ma come in questo caso foreach (var url in await GetUrls ()) {yield return GetContent (url)}; Ho trovato solo un modo per dividere in due metodi.
Juan Pablo Garcia Coello


-1

Questa soluzione funziona come previsto. Nota la await Task.Run(() => enumerator.MoveNext())parte.

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;
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.