É possível “esperar retorno de rendimento DoSomethingAsync ()”

108

Os blocos de iteradores regulares (ou seja, "retorno de rendimento") são incompatíveis com "async" e "await"?

Isso dá uma boa ideia do que estou tentando fazer:

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

No entanto, recebo um erro do compilador citando "não foi possível carregar a string da mensagem dos recursos".

Aqui está outra tentativa:

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

Mas, novamente, o compilador retorna um erro: "não foi possível carregar a string da mensagem dos recursos".


Aqui está o código de programação real do meu projeto

Isso é muito útil quando eu tenho uma tarefa de lista, essa tarefa pode ser baixar HTML de uma URL e eu uso a sintaxe "yield return await task", o resultado é eu quero IEnumerable<Foo>. Não quero escrever 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;
}

Mas parece que preciso.

Obrigado por qualquer ajuda.

Jiangzhen
fonte
4
Esta questão não é particularmente clara, você deve elaborar sobre o que exatamente você deseja fazer - é difícil determinar o que você deseja fazer apenas com um exemplo de código sozinho.
Justin
3
O pacote NuGet Ix_Experimental-Async inclui "enumeradores assíncronos" completos com suporte LINQ. Eles usam o IAsyncEnumerator<T>tipo definido por Arne.
Stephen Cleary
@StephenCleary esse pacote foi removido da lista. Já que isso foi há muito tempo e de autoria da rxteam da MS, você sabe se isso foi incluído no RX?
JoeBrockhaus de
@JoeBrockhaus: Parece que continua como Ix-Async .
Stephen Cleary de

Respostas:

72

O que você está descrevendo pode ser realizado com o Task.WhenAllmétodo. Observe como o código se transforma em uma linha simples. O que acontece é que cada url individual começa a baixar e então WhenAllé usado para combinar essas operações em uma única Taskque pode ser aguardada.

Task<IEnumerable<string>> DownLoadAllUrls(string[] urls)
{
    return Task.WhenAll(from url in urls select DownloadHtmlAsync(url));
}
Brian Gideon
fonte
10
async / await não são necessários neste caso. Apenas retire asyncdo método e faça return Task.WhenAlldiretamente.
luiscubal
22
A última linha pode ser escrita de forma mais sucinta comourls.Select(DownloadHtmlAsync)
BlueRaja - Danny Pflughoeft
1
Este acabou sendo o problema exato que eu estava enfrentando (baixando lentamente strings de uma série de URIs). Obrigado.
James Ko
13
Na verdade, isso não responde à pergunta: Is it possible to await yield? Você acabou de encontrar uma solução para este caso de uso. Não respondeu à pergunta geral.
Buh Buh
1
Eu estaria inclinado a retornar, Task<string[]>pois isso indicaria que você não está mais retornando um iterador com execução adiada ie. todos os URLs estão sendo baixados.
johnnycardy
73

tl; dr Iteradores implementados com rendimento são uma construção de bloqueio, portanto, a partir de agora, await e rendimento são incompatíveis.

Longo Como iterar sobre uma IEnumerableé uma operação de bloqueio, chamar um método marcado como asyncainda o executará de maneira bloqueadora, pois terá que aguardar o término da operação.

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

A espera Methodmistura significados. Você deseja aguardar até que Tasktenha um IEnumerablee, em seguida, bloquear a iteração sobre ele? Ou você está tentando aguardar cada valor do IEnumerable?

Presumo que o segundo seja o comportamento desejado e, nesse caso, a semântica do Iterator existente não funcionará. A IEnumerator<T>interface é basicamente

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

Estou ignorando, Reset()pois não faz sentido para uma sequência de resultados assíncronos. Mas o que você precisa é algo assim:

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

Claro, foreachtambém não funcionará com isso e você terá que iterar manualmente assim:

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
fonte
1
Você acha que é possível que a próxima versão da linguagem C # adicione foreachsuporte para sua hipótese IAsyncEnumerator<T>?
Dai de
1
@Dai Eu acho que está em desenvolvimento. para C # 8.0 / Li sobre isso.
nawfal
40

De acordo com os novos recursos do C # 8.0 ( link # 1 e link # 2 ), teremos IAsyncEnumerable<T>suporte de interface que permitirá a implementação de sua segunda tentativa. Isso parecerá assim:

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 obter o mesmo comportamento em C # 5, mas com uma 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);
}

A resposta de Brian Gideon implica que o código de chamada obterá de forma assíncrona uma coleção de resultados obtidos em paralelo. O código acima implica que o código de chamada obterá resultados de um fluxo um por um de maneira assíncrona.

AlbertK
fonte
17

Sei que estou muito atrasado com a resposta, mas aqui está outra solução simples que pode ser alcançada com esta biblioteca:
GitHub: https://github.com/tyrotoxin/AsyncEnumerable
NuGet.org: https: //www.nuget .org / packages / AsyncEnumerator /
É muito mais simples 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
fonte
5

Este recurso estará disponível a partir do C # 8.0. https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

Do MSDN

Streams assíncronos

O recurso async / await do C # 5.0 permite que você consuma (e produza) resultados assíncronos em código direto, sem retornos de chamada:

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

Não é tão útil se você deseja consumir (ou produzir) fluxos contínuos de resultados, como você pode obter de um dispositivo IoT ou um serviço de nuvem. Os streams assíncronos existem para isso.

Apresentamos IAsyncEnumerable, que é exatamente o que você esperaria; uma versão assíncrona de IEnumerable. A linguagem permite que você aguarde foreach sobre estes para consumir seus elementos e render retorno a eles para produzir elementos.

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

Em primeiro lugar, tenha em mente que o material do Async não está concluído. A equipe C # ainda tem um longo caminho a percorrer antes que o C # 5 seja lançado.

Dito isso, acho que você pode querer reunir as tarefas que estão sendo disparadas na DownloadAllHtmlfunção de uma maneira diferente.

Por exemplo, você pode usar algo assim:

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

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

Não que a DownloadAllUrlfunção NÃO seja uma chamada assíncrona. Mas, você pode ter a chamada assíncrona implementada em outra função (ou seja DownloadHtmlAsync).

A Biblioteca Paralela de Tarefas tem as funções .ContinueWhenAnye .ContinueWhenAll.

Isso pode ser usado assim:

var tasks = DownloadAllUrl(...);
var tasksArray = tasks.ToArray();
var continuation = Task.Factory.ContinueWhenAll(tasksArray, completedTasks =>
{
    completedtask
});
continuation.RunSynchronously();
John Gietzen
fonte
Você também pode ter um 'cabeçalho' (antes do loop) se definir seu método como assíncrono Task<IEnumerable<Task<string>>> DownloadAllUrl. Ou, se você quiser ações de 'rodapé' IEnumerable<Task>. Por exemplo, gist.github.com/1184435
Jonathan Dickinson
1
Agora que o asyncmaterial foi concluído e o C # 5.0 foi lançado, isso pode ser atualizado.
casperOne
Eu gosto deste, mas como neste caso foreach (var url em await GetUrls ()) {yield return GetContent (url)}; Só encontrei uma maneira de dividir em dois métodos.
Juan Pablo Garcia Coello,
-1

Esta solução funciona conforme o esperado. Observe a 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;
        }
    }
}
François
fonte