Apakah mungkin untuk "menunggu hasil pengembalian DoSomethingAsync ()"

108

Apakah blok iterator biasa (yaitu "yield return") tidak kompatibel dengan "async" dan "await"?

Ini memberi gambaran bagus tentang apa yang saya coba lakukan:

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

Namun, saya mendapatkan kesalahan kompiler yang mengutip "tidak dapat memuat string pesan dari sumber".

Ini upaya lainnya:

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

Tetapi sekali lagi, kompilator mengembalikan kesalahan: "tidak dapat memuat string pesan dari sumber daya".


Ini adalah kode pemrograman sebenarnya dalam proyek saya

Ini sangat berguna ketika saya memiliki Tugas Daftar, tugas itu bisa mendownload HTML dari URL dan saya menggunakan sintaks "yield return await task", hasilnya saya inginkan IEnumerable<Foo>. Saya tidak ingin menulis kode ini:

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

Tapi sepertinya aku harus melakukannya.

Terima kasih atas bantuannya.

jiangzhen
sumber
4
Pertanyaan ini tidak terlalu jelas, Anda harus menguraikan apa yang sebenarnya ingin Anda lakukan - sulit untuk menentukan apa yang ingin Anda lakukan hanya dari sampel kode saja.
Justin
3
Paket Ix_Experimental-Async NuGet menyertakan "enumerator asinkron" lengkap dengan dukungan LINQ. Mereka menggunakan IAsyncEnumerator<T>tipe seperti yang didefinisikan oleh Arne.
Stephen Cleary
@StephenCleary bahwa paket telah dihapus dari daftar. Karena ini sudah lama sekali & ditulis oleh MS's rxt displaytext, tahukah Anda jika hal itu dimasukkan ke dalam RX?
JoeBrockhaus
@JoeBrockhaus: Sepertinya ia hidup sebagai Ix-Async .
Stephen Cleary

Jawaban:

72

Apa yang Anda gambarkan dapat dicapai dengan Task.WhenAllmetode ini. Perhatikan bagaimana kode berubah menjadi satu baris sederhana. Apa yang terjadi adalah setiap url individu mulai mengunduh dan kemudian WhenAlldigunakan untuk menggabungkan operasi tersebut menjadi satu Taskyang dapat ditunggu.

Task<IEnumerable<string>> DownLoadAllUrls(string[] urls)
{
    return Task.WhenAll(from url in urls select DownloadHtmlAsync(url));
}
Brian Gideon
sumber
10
async / await tidak diperlukan dalam kasus ini. Hapus saja asyncdari metode dan lakukan return Task.WhenAllsecara langsung.
luiscubal
22
Baris terakhir dapat ditulis lebih ringkas sebagaiurls.Select(DownloadHtmlAsync)
BlueRaja - Danny Pflughoeft
1
Ternyata ini adalah masalah yang sebenarnya saya hadapi (dengan malas mengunduh string dari serangkaian URI). Terima kasih.
James Ko
13
Ini tidak benar-benar menjawab pertanyaan Is it possible to await yield? Anda baru saja menemukan solusi untuk kasus penggunaan yang satu ini. Tidak menjawab pertanyaan umum.
Buh Buh
1
Saya akan cenderung untuk kembali Task<string[]>karena ini akan menunjukkan bahwa Anda tidak lagi mengembalikan iterator dengan eksekusi yang ditangguhkan yaitu. semua URL sedang diunduh.
johnnycardy
73

tl; dr Iterators yang diimplementasikan dengan yield adalah konstruksi pemblokiran, sehingga untuk saat ini await dan yield tidak kompatibel.

Panjang Karena mengulangiIEnumerable operasi pemblokiran, memanggil metode yang ditandai sebagai asyncakan tetap mengeksekusinya dengan cara pemblokiran, karena harus menunggu hingga operasi tersebut selesai.

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

Menunggu MethodArti campuran . Apakah Anda ingin menunggu sampai Taskmemiliki IEnumerabledan kemudian memblokir iterasinya? Atau apakah Anda mencoba menunggu setiap nilai IEnumerable?

Saya berasumsi yang kedua adalah perilaku yang diinginkan dan dalam hal ini semantik Iterator yang ada tidak akan berfungsi. ItuIEnumerator<T> antarmuka pada dasarnya adalah

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

Saya mengabaikan Reset() karena tidak masuk akal untuk urutan hasil asinkron. Tetapi yang Anda butuhkan adalah sesuatu seperti ini:

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

Tentu saja, foreachjuga tidak akan berfungsi dengan ini dan Anda harus mengulang secara manual seperti ini:

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
sumber
1
Menurut Anda, apakah mungkin versi bahasa C # berikutnya akan menambah foreachdukungan untuk hipotesis Anda IAsyncEnumerator<T>?
Dai
1
@Dai Saya pikir itu sangat banyak di dalam pipa. untuk C # 8.0 / Telah membaca tentang itu.
nawfal
40

Menurut fitur baru di C # 8.0 ( tautan # 1 dan tautan # 2 ) kami akan memiliki IAsyncEnumerable<T>dukungan antarmuka yang memungkinkan Anda untuk mengimplementasikan upaya kedua Anda. Ini akan terlihat seperti ini:

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

Kita dapat mencapai perilaku yang sama di C # 5 tetapi dengan semantik yang berbeda:

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

Jawaban Brian Gideon menyiratkan bahwa kode panggilan akan mendapatkan sekumpulan hasil yang diperoleh secara paralel secara asinkron. Kode di atas menyiratkan bahwa kode pemanggil akan mendapatkan hasil seperti dari streaming satu per satu secara asynchronous.

AlbertK
sumber
17

Saya tahu bahwa saya terlambat dengan jawabannya, tetapi berikut adalah solusi sederhana lain yang dapat dicapai dengan pustaka ini:
GitHub: https://github.com/tyrotoxin/AsyncEnumerable
NuGet.org: https: //www.nuget .org / packages / AsyncEnumerator /
Jauh lebih sederhana daripada 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
sumber
5

Fitur ini akan tersedia mulai C # 8.0. https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

Dari MSDN

Aliran asinkron

Fitur async / await C # 5.0 memungkinkan Anda menggunakan (dan menghasilkan) hasil asinkron dalam kode langsung, tanpa callback:

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

Tidak terlalu membantu jika Anda ingin menggunakan (atau menghasilkan) aliran hasil yang berkelanjutan, seperti yang mungkin Anda dapatkan dari perangkat IoT atau layanan cloud. Aliran asinkron tersedia untuk itu.

Kami memperkenalkan IAsyncEnumerable, yang persis seperti yang Anda harapkan; versi IEnumerable yang tidak sinkron. Bahasa memungkinkan Anda menunggu untuk masing-masing ini untuk mengkonsumsi elemen mereka, dan menghasilkan kembali kepada mereka untuk menghasilkan elemen.

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

Pertama-tama, perlu diingat bahwa hal-hal Async belum selesai. Perjalanan tim C # masih panjang sebelum C # 5 dirilis.

Karena itu, saya pikir Anda mungkin ingin mengumpulkan tugas yang dipecat dalam DownloadAllHtmlfungsi dengan cara yang berbeda.

Misalnya, Anda dapat menggunakan sesuatu seperti ini:

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

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

Bukan berarti DownloadAllUrlfungsinya BUKAN panggilan async. Namun, Anda dapat menerapkan panggilan async pada fungsi lain (yaitu DownloadHtmlAsync).

Task Parallel Library memiliki fungsi .ContinueWhenAnydan .ContinueWhenAll.

Itu bisa digunakan seperti ini:

var tasks = DownloadAllUrl(...);
var tasksArray = tasks.ToArray();
var continuation = Task.Factory.ContinueWhenAll(tasksArray, completedTasks =>
{
    completedtask
});
continuation.RunSynchronously();
John Gietzen
sumber
Anda juga dapat memiliki 'header' (sebelum loop) jika Anda mendefinisikan metode Anda sebagai async Task<IEnumerable<Task<string>>> DownloadAllUrl. Atau, jika Anda menginginkan tindakan 'footer' IEnumerable<Task>. Misalnya gist.github.com/1184435
Jonathan Dickinson
1
Sekarang bahwa asynchal ini selesai dan C # 5.0 adalah dirilis, ini dapat diperbarui.
casperOne
Saya suka yang ini, tapi bagaimana dalam kasus ini foreach (var url in await GetUrls ()) {yield return GetContent (url)}; Saya hanya menemukan cara membelah dalam dua metode.
Juan Pablo Garcia Coello
-1

Solusi ini berfungsi seperti yang diharapkan. Perhatikan await Task.Run(() => enumerator.MoveNext())bagiannya.

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
sumber