[c#] “Await 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>. 결과는 I want 입니다. 이 코드를 작성하고 싶지 않습니다.

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

하지만해야 할 것 같습니다.

도움을 주셔서 감사합니다.



답변

설명하는 내용은 Task.WhenAll방법 으로 수행 할 수 있습니다 . 코드가 어떻게 단순한 한 줄로 변하는 지 주목하십시오. 발생하는 것은 각 개별 URL이 다운로드를 시작한 다음 WhenAll이러한 작업을 Task기다릴 수 있는 단일로 결합하는 데 사용됩니다 .

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


답변

tl; dr yield로 구현 된 반복자는 차단 구조이므로 지금은 대기하고 yield는 호환되지 않습니다.

Long을 반복하는 IEnumerable것은 차단 작업이므로로 표시된 메서드를 호출하면 async해당 작업이 완료 될 때까지 기다려야하므로 여전히 차단 방식으로 실행됩니다.

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

기다리는 것은 Method의미를 혼합합니다. 당신은이 때까지 기다리시겠습니까 Task을 가지고 IEnumerable다음의 반복에 차단? 아니면의 각 값을 기다리려고 IEnumerable합니까?

두 번째가 원하는 동작이라고 가정하고이 경우 기존 반복자 의미 체계가 작동하지 않습니다. 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();
}


답변

C # 8.0 의 새로운 기능 ( link # 1link # 2 )에 IAsyncEnumerable<T>따르면 두 번째 시도를 구현할 수 있는 인터페이스 지원이 제공됩니다. 다음과 같이 표시됩니다.

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

Brian Gideon의 대답은 호출 코드가 병렬로 얻은 결과 모음을 비동기 적으로 가져올 것임을 의미합니다. 위의 코드는 호출 코드가 비동기 방식으로 하나씩 스트림에서 결과를 얻을 수 있음을 의미합니다.


답변

답변에 너무 늦었다는 것을 알고 있지만 여기에이 라이브러리로 얻을 수있는 또 다른 간단한 솔루션이 있습니다.
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);
  });
}


답변

이 기능은 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;
    }
}


답변

할 계획이 있었다

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

하지만 현재는 불가능합니다


답변

우선, 비동기 작업이 완료되지 않았 음을 명심하십시오. 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함수가 비동기 호출 이 아니라는 것은 아닙니다 . 그러나 다른 함수에 비동기 호출을 구현할 수 있습니다 (예 :DownloadHtmlAsync .

태스크 병렬 라이브러리에는 .ContinueWhenAny.ContinueWhenAll 기능이 있습니다.

다음과 같이 사용할 수 있습니다.

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