¿Es posible "esperar el retorno de rendimiento DoSomethingAsync ()"

108

¿Los bloques de iteradores regulares (es decir, "rendimiento de retorno") son incompatibles con "async" y "await"?

Esto da una buena idea de lo que estoy tratando de hacer:

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

Sin embargo, obtengo un error del compilador que menciona "no se puede cargar la cadena de mensajes desde los recursos".

Aquí hay otro intento:

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

Pero nuevamente, el compilador devuelve un error: "no se puede cargar la cadena de mensajes desde los recursos".


Aquí está el código de programación real en mi proyecto.

Esto es muy útil cuando tengo una tarea de lista, esa tarea se puede descargar HTML desde una URL y uso la sintaxis "rendimiento, retorno, espera de tarea", el resultado es que quiero IEnumerable<Foo>. No quiero escribir este código:

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

Pero parece que tengo que hacerlo.

Gracias por cualquier ayuda.

jiangzhen
fuente
4
Esta pregunta no es particularmente clara, debe detallar qué es exactamente lo que desea hacer; es difícil determinar qué es lo que desea hacer solo con una muestra de código.
Justin
3
El paquete Ix_Experimental-Async NuGet incluye "enumeradores asincrónicos" completos con soporte LINQ. Usan el IAsyncEnumerator<T>tipo definido por Arne.
Stephen Cleary
@StephenCleary ese paquete ha sido eliminado de la lista. Dado que esto fue hace años y fue escrito por rxteam de MS, ¿sabe si se incorporó a RX?
JoeBrockhaus
@JoeBrockhaus: Parece que sigue vivo como Ix-Async .
Stephen Cleary

Respuestas:

72

Lo que está describiendo se puede lograr con el Task.WhenAllmétodo. Observe cómo el código se convierte en una simple frase. Lo que sucede es que cada URL individual comienza a descargarse y luego WhenAllse usa para combinar esas operaciones en una única Taskque se puede esperar.

Task<IEnumerable<string>> DownLoadAllUrls(string[] urls)
{
    return Task.WhenAll(from url in urls select DownloadHtmlAsync(url));
}
Brian Gideon
fuente
10
async / await no son necesarios en este caso. Simplemente elimínelo asyncdel método y hágalo return Task.WhenAlldirectamente.
luiscubal
22
La última línea se puede escribir de manera más sucinta comourls.Select(DownloadHtmlAsync)
BlueRaja - Danny Pflughoeft
1
Este resultó ser el problema exacto al que me estaba enfrentando (descargando perezosamente cadenas de una serie de URI). Gracias.
James Ko
13
En realidad, esto no responde a la pregunta de Is it possible to await yield? que acaba de encontrar una solución alternativa para este caso de uso. No respondió la pregunta general.
Buh Buh
1
Me inclinaría a regresar, Task<string[]>ya que esto indicaría que ya no está devolviendo un iterador con ejecución diferida, es decir. se están descargando todas las URL.
johnnycardy
73

tl; dr Los iteradores implementados con yield son una construcción de bloqueo, por lo que a partir de ahora, await y yield son incompatibles.

Long Debido a que iterar sobre una IEnumerablees una operación de bloqueo, llamar a un método marcado como asynctodavía lo ejecutará de manera de bloqueo, ya que tiene que esperar a que termine esa operación.

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

La espera Methodmezcla significados. ¿Quieres esperar hasta que Tasktenga un IEnumerabley luego bloquear la iteración sobre él? ¿O está tratando de esperar cada valor de IEnumerable?

Supongo que el segundo es el comportamiento deseado y, en ese caso, la semántica del iterador existente no funcionará. La IEnumerator<T>interfaz es básicamente

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

Lo ignoro Reset()porque no tiene sentido para una secuencia de resultados asincrónicos. Pero lo que necesitarías es algo como esto:

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

Por supuesto, foreachtampoco funcionará con esto y tendrías que iterar manualmente de esta manera:

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
fuente
1
¿Crees que es posible que la próxima versión en lenguaje C # agregue foreachsoporte para tu hipotética IAsyncEnumerator<T>?
Dai
1
@Dai Creo que está en proceso. para C # 8.0 / Había leído sobre ello.
Nawfal
40

De acuerdo con las nuevas características en C # 8.0 ( enlace # 1 y enlace # 2 ) tendremos IAsyncEnumerable<T>soporte de interfaz que permitirá implementar su segundo intento. Se verá así:

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

Podemos lograr el mismo comportamiento en C # 5 pero con una semántica diferente:

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 respuesta de Brian Gideon implica que el código de llamada obtendrá de forma asincrónica una colección de resultados que se obtuvieron en paralelo. El código anterior implica que el código de llamada obtendrá resultados como de una secuencia uno por uno de manera asincrónica.

AlbertK
fuente
17

Sé que llego demasiado tarde con la respuesta, pero aquí hay otra solución simple que se puede lograr con esta biblioteca:
GitHub: https://github.com/tyrotoxin/AsyncEnumerable
NuGet.org: https: //www.nuget .org / packages / AsyncEnumerator /
Es mucho más 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
fuente
5

Esta función estará disponible a partir de C # 8.0. https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

De MSDN

Secuencias asincrónicas

La característica async / await de C # 5.0 le permite consumir (y producir) resultados asincrónicos en código sencillo, sin devoluciones de llamada:

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

No es tan útil si desea consumir (o producir) flujos continuos de resultados, como los que podría obtener de un dispositivo IoT o un servicio en la nube. Las transmisiones asíncronas están ahí para eso.

Presentamos IAsyncEnumerable, que es exactamente lo que esperaría; una versión asincrónica de IEnumerable. El lenguaje te permite esperar a que cada uno de estos consuma sus elementos y les dé retorno para producir elementos.

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

En primer lugar, tenga en cuenta que las cosas de Async no están terminadas. El equipo de C # todavía tiene un largo camino por recorrer antes de que se lance C # 5.

Dicho esto, creo que es posible que desee recopilar las tareas que se están ejecutando en la DownloadAllHtmlfunción de una manera diferente.

Por ejemplo, puede usar algo como esto:

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

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

No es que la DownloadAllUrlfunción NO sea una llamada asíncrona. Pero, puede implementar la llamada asíncrona en otra función (es decir DownloadHtmlAsync).

La biblioteca de tareas paralelas tiene las funciones .ContinueWhenAnyy .ContinueWhenAll.

Eso se puede usar así:

var tasks = DownloadAllUrl(...);
var tasksArray = tasks.ToArray();
var continuation = Task.Factory.ContinueWhenAll(tasksArray, completedTasks =>
{
    completedtask
});
continuation.RunSynchronously();
John Gietzen
fuente
También puede tener un 'encabezado' (antes del bucle) si define su método como asíncrono Task<IEnumerable<Task<string>>> DownloadAllUrl. O, si desea acciones de 'pie de página' IEnumerable<Task>. Por ejemplo, gist.github.com/1184435
Jonathan Dickinson
1
Ahora que asynctodo está terminado y se lanza C # 5.0 , esto se puede actualizar.
casperOne
Me gusta este, pero cómo en este caso foreach (var url in await GetUrls ()) {yield return GetContent (url)}; Solo encontré una manera de dividirme en dos métodos.
Juan Pablo García Coello
-1

Esta solución funciona como se esperaba. Tenga en cuenta 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;
        }
    }
}
Francois
fuente