web-dev-qa-db-ja.com

「yield return DoSomethingAsync()を待つ」ことは可能ですか

通常のイテレータブロック(つまり、「yield return」)は「async」および「await」と互換性がありませんか?

これは私がやろうとしていることの良いアイデアを提供します:

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

しかし、「リソースからメッセージ文字列を読み込めません」と言うコンパイラエラーが発生します。

別の試みを次に示します。

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

しかし、コンパイラは再びエラーを返します:「リソースからメッセージ文字列をロードできません」。


これが私のプロジェクトの実際のプログラミングコードです

これは、リストタスクがあり、そのタスクがURLからHTMLをダウンロードでき、「yield return await task」という構文を使用する場合に非常に便利です。結果はIEnumerable<Foo>。私はこのコードを書きたくありません:

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

しかし、そうしなければならないようです。

助けてくれてありがとう。

93
jiangzhen

説明していることは、Task.WhenAll 方法。コードがどのように単純なワンライナーに変わるかに注目してください。個々のURLのダウンロードが開始され、WhenAllが使用されて、これらの操作が単一のTaskに結合され、待機できます。

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

tl; dr yieldで実装されたイテレータはブロッキング構造であるため、現時点ではawaitとyieldは互換性がありません。

LongIEnumerableを反復処理するのはブロッキング操作であるため、asyncとしてマークされたメソッドを呼び出すと、それを待つ必要があるため、ブロック方式で実行されます。終了する操作。

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

待っているMethodは意味を混ぜています。 TaskIEnumerableが含まれるまで待ってから、その繰り返しをブロックしますか?または、IEnumerableの各値を待機しようとしていますか?

2番目は望ましい動作であり、その場合、既存のIteratorセマンティクスは機能しないと思います。 _IEnumerator<T>_インターフェイスは基本的に

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

Reset()は無視します。非同期結果のシーケンスには意味がないためです。ただし、必要なのは次のようなものです。

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

もちろん、foreachもこれでは機能せず、次のように手動で繰り返す必要があります。

_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();
}
_
67
Arne Claassen

C#8.0link#1 and link#2 )の新機能によると、 'IAsyncEnumerable<T>インターフェイスのサポートにより、2回目の試行を実装できます。次のようになります。

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

C#5で同じ動作を実現できますが、セマンティクスは異なります。

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

ブライアンギデオンの答えは、呼び出し元のコードが非同期に並行して得られた結果のコレクションを取得することを意味します。上記のコードは、呼び出し元のコードがストリームのような結果を非同期的に1つずつ取得することを意味しています。

22
AlbertK

私は答えに遅すぎることを知っていますが、このライブラリで達成できる別の簡単な解決策があります:
GitHub: https://github.com/tyrotoxin/AsyncEnumerable
NuGet.org: https://www.nuget.org/packages/AsyncEnumerator/
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);
  });
}
17
Serge Semenov

する計画がありました

https://github.com/dotnet/csharplang/issues/4

しかし、現在は不可能です

4
Thaina

この機能は、C#8.0以降で使用可能になります。 https://blogs.msdn.Microsoft.com/dotnet/2018/11/12/building-c-8-0/

MSDNから

非同期ストリーム

C#5.0のasync/await機能を使用すると、コールバックなしで、簡単なコードで非同期結果を使用(および生成)できます。

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

IoTデバイスやクラウドサービスから取得するような結果の連続的なストリームを消費(または生成)したい場合、あまり役に立ちません。そのために非同期ストリームがあります。

IAsyncEnumerableを導入します。これはまさにあなたが期待するものです。 IEnumerableの非同期バージョン。この言語では、これらの要素のforeachを待って要素を消費し、要素に戻るためにそれらに戻ります。

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

残念ながら、yieldは待ち状態では機能しません。しかし、これがRxの目的です。チェックアウト https://msdn.Microsoft.com/library/hh242985

1
Johan Franzén

まず、非同期のものが終了していないことに注意してください。 C#5がリリースされるまで、C#チームにはまだ長い道のりがあります。

そうは言っても、別の方法でDownloadAllHtml関数で実行されるタスクを収集したいと思うかもしれません。

たとえば、次のようなものを使用できます。

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

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

DownloadAllUrl関数が[〜#〜] not [〜#〜]非同期呼び出しであることではありません。ただし、非同期呼び出しを別の関数(つまり、DownloadHtmlAsync)に実装することはできます。

タスク並列ライブラリには.ContinueWhenAnyおよび .ContinueWhenAll 関数。

これは次のように使用できます。

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