Czy można „czekać na zwrot wyniku DoSomethingAsync ()”

108

Czy zwykłe bloki iteratora (np. „Yield return”) są niezgodne z „async” i „await”?

To daje dobre wyobrażenie o tym, co próbuję zrobić:

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

Jednak pojawia się błąd kompilatora, powołujący się na „nie można załadować ciągu wiadomości z zasobów”.

Oto kolejna próba:

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

Ale znowu kompilator zwraca błąd: „nie można załadować ciągu komunikatów z zasobów”.


Oto prawdziwy kod programistyczny w moim projekcie

Jest to bardzo przydatne, gdy mam zadanie listy, to zadanie można pobrać HTML z adresu URL i używam składni „yield return await task”, czego oczekuję IEnumerable<Foo>. Nie chcę pisać tego kodu:

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

Ale wydaje się, że muszę.

Dzięki za wszelką pomoc.

jiangzhen
źródło
4
To pytanie nie jest szczególnie jasne, powinieneś szczegółowo opisać, co dokładnie chcesz zrobić - trudno jest określić, co chcesz zrobić, na podstawie samej próbki kodu.
Justin,
3
Pakiet Ix_Experimental-Async NuGet zawiera „asynchroniczne moduły wyliczające” wraz z obsługą LINQ. Używają IAsyncEnumerator<T>typu zdefiniowanego przez Arne'a.
Stephen Cleary
@StephenCleary ten pakiet został usunięty z listy. Ponieważ to było wieki temu i napisane przez rxteam MS, czy wiesz, czy to zostało wprowadzone do RX?
JoeBrockhaus
@JoeBrockhaus: Wygląda na to, że żyje dalej jako Ix-Async .
Stephen Cleary

Odpowiedzi:

72

To, co opisujesz, można osiągnąć za pomocą tej Task.WhenAllmetody. Zwróć uwagę, jak kod zamienia się w prostą jednowierszową. Dzieje się tak, że każdy indywidualny adres URL zaczyna się pobierać, a następnie WhenAlljest używany do łączenia tych operacji w jeden, na Taskktóry można czekać.

Task<IEnumerable<string>> DownLoadAllUrls(string[] urls)
{
    return Task.WhenAll(from url in urls select DownloadHtmlAsync(url));
}
Brian Gideon
źródło
10
async / await nie są potrzebne w tym przypadku. Po prostu usuń asyncz metody i zrób return Task.WhenAllbezpośrednio.
luiscubal
22
Ostatnią linijkę można napisać bardziej zwięźle, ponieważurls.Select(DownloadHtmlAsync)
BlueRaja - Danny Pflughoeft
1
Okazało się, że był to dokładnie ten problem, z którym miałem do czynienia (leniwe pobieranie ciągów z serii identyfikatorów URI). Dziękuję Ci.
James Ko,
13
To właściwie nie odpowiada na pytanie, Is it possible to await yield? że właśnie znalazłeś obejście tego jednego przypadku użycia. Nie odpowiedział na ogólne pytanie.
Buh Buh
1
Byłbym skłonny do powrotu, Task<string[]>ponieważ oznaczałoby to, że nie zwracasz już iteratora z odroczonym wykonaniem, tj. wszystkie adresy URL są pobierane.
johnnycardy
73

tl; dr Iteratory zaimplementowane z wydajnością są konstrukcją blokującą, więc w tej chwili oczekiwanie i wydajność są niekompatybilne.

Long Ponieważ iteracja po operacji IEnumerablejest operacją blokującą, wywołanie metody oznaczonej jako asyncnadal będzie ją wykonywać w sposób blokujący, ponieważ musi czekać na zakończenie tej operacji.

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

Oczekiwanie Methodmiesza znaczenia. Czy chcesz poczekać, aż Taskpojawi IEnumerablesię ikona, a następnie zablokować jej iterację? A może próbujesz poczekać na każdą wartość IEnumerable?

Zakładam, że drugim jest pożądane zachowanie iw takim przypadku istniejąca semantyka Iteratora nie będzie działać. IEnumerator<T>Interfejs jest w zasadzie

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

Ignoruję, Reset()ponieważ nie ma to sensu w przypadku sekwencji wyników asynchronicznych. Ale to, czego potrzebujesz, to coś takiego:

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

Oczywiście, foreachteż z tym nie zadziała i będziesz musiał wykonać iterację ręcznie w ten sposób:

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
źródło
1
Czy uważasz, że jest możliwe, że następna wersja językowa C # doda foreachobsługę Twojej hipotetycznej IAsyncEnumerator<T>?
Dai
1
@Dai Myślę, że jest w przygotowaniu. dla C # 8.0 / Czytałem o tym.
nawfal
40

Zgodnie z nowymi funkcjami w C # 8.0 ( łącze nr 1 i łącze nr 2 ) będziemy mieć IAsyncEnumerable<T>obsługę interfejsu, która umożliwi zaimplementowanie drugiej próby. Będzie to wyglądać tak:

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

Możemy osiągnąć to samo zachowanie w C # 5, ale z inną semantyką:

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

Odpowiedź Briana Gideona sugeruje, że wywołujący kod otrzyma asynchronicznie zbiór wyników uzyskanych równolegle. Powyższy kod oznacza, że ​​kod wywołujący będzie uzyskiwał wyniki, takie jak ze strumienia jeden po drugim w sposób asynchroniczny.

AlbertK
źródło
17

Wiem, że jestem za późno z odpowiedzią, ale oto kolejne proste rozwiązanie, które można osiągnąć dzięki tej bibliotece:
GitHub: https://github.com/tyrotoxin/AsyncEnumerable
NuGet.org: https: //www.nuget .org / packages / AsyncEnumerator /
To znacznie prostsze niż 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
źródło
5

Ta funkcja będzie dostępna od C # 8.0. https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

Z MSDN

Strumienie asynchroniczne

Funkcja async / await w C # 5.0 umożliwia konsumowanie (i tworzenie) wyników asynchronicznych w prostym kodzie, bez wywołań zwrotnych:

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

Nie jest to tak pomocne, jeśli chcesz konsumować (lub wytwarzać) ciągłe strumienie wyników, takie jak można uzyskać z urządzenia IoT lub usługi w chmurze. Do tego służą strumienie asynchroniczne.

Przedstawiamy IAsyncEnumerable, czyli dokładnie to, czego można się spodziewać; asynchroniczna wersja IEnumerable. Język pozwala ci czekać na wszystko, aż konsumują ich elementy i zwracają je do produkcji elementów.

async IAsyncEnumerable<int> GetBigResultsAsync()
{
    await foreach (var result in GetResultsAsync())
    {
        if (result > 20) yield return result; 
    }
}
Petter Pettersson
źródło
1

Przede wszystkim pamiętaj, że rzeczy Async nie są skończone. Zespół języka C # wciąż ma przed sobą długą drogę do wydania języka C # 5.

Biorąc to pod uwagę, myślę, że możesz chcieć zebrać zadania, które są zwolnione w DownloadAllHtmlfunkcji w inny sposób.

Na przykład możesz użyć czegoś takiego:

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

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

Nie oznacza to, że DownloadAllUrlfunkcja NIE jest wywołaniem asynchronicznym. Ale możesz mieć wywołanie asynchroniczne zaimplementowane w innej funkcji (tj DownloadHtmlAsync.).

Biblioteka równoległa zadań ma funkcje .ContinueWhenAnyi .ContinueWhenAll.

Można tego użyć w następujący sposób:

var tasks = DownloadAllUrl(...);
var tasksArray = tasks.ToArray();
var continuation = Task.Factory.ContinueWhenAll(tasksArray, completedTasks =>
{
    completedtask
});
continuation.RunSynchronously();
John Gietzen
źródło
Możesz również mieć „nagłówek” (przed pętlą), jeśli zdefiniujesz swoją metodę jako asynchroniczną Task<IEnumerable<Task<string>>> DownloadAllUrl. Lub, jeśli chcesz, aby akcje „stopki” IEnumerable<Task>. Np. Gist.github.com/1184435
Jonathan Dickinson
1
Teraz, gdy asyncwszystko jest skończone i C # 5.0 jest wydany, można to zaktualizować.
casper 1
Podoba mi się ten, ale jak w tym przypadku foreach (var url in await GetUrls ()) {yield return GetContent (url)}; Znalazłem tylko sposób na podział na dwie metody.
Juan Pablo Garcia Coello
-1

To rozwiązanie działa zgodnie z oczekiwaniami. Zwróć uwagę na await Task.Run(() => enumerator.MoveNext())część.

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
źródło