是否可以“等待收益回报DoSomethingAsync()”


108

常规迭代器块(即“收益回报”)是否与“异步”和“等待”不兼容?

这样可以很好地说明我要做什么:

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

但似乎我必须这样做。

谢谢你的帮助。


4
这个问题不是特别清楚,您应该详细说明您希望执行的操作-仅凭代码示例很难确定您要执行的操作。
贾斯汀

3
Ix_Experimental-异步NuGet包包括“异步普查员”完整的LINQ支持。他们使用IAsyncEnumerator<T>Arne定义的类型。
Stephen Cleary

@StephenCleary该软件包已被除名。由于这是由MS的rxteam编写的很久以前的内容,您知道它是否被纳入RX?
JoeBrockhaus

@JoeBrockhaus:看起来像Ix-Async一样
史蒂芬·克雷里

Answers:


72

您所描述的内容可以通过该Task.WhenAll方法完成。请注意,代码是如何变成简单的单行代码的。发生的情况是每个单独的url开始下载,然后WhenAll用于将这些操作组合为一个Task可以等待的操作。

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

10
在这种情况下,不需要异步/等待。只需async从方法中删除并return Task.WhenAll直接执行即可。
luiscubal

22
最后一行可以写得更简洁:urls.Select(DownloadHtmlAsync)
BlueRaja-Danny Pflughoeft

1
原来这是我面临的确切问题(从一系列URI延迟下载字符串)。谢谢。
詹姆斯·柯

13
这实际上并没有回答“ Is it possible to await yield? 您刚刚找到了这个用例的变通方法” 的问题。没有回答一般性问题。
Buh Buh

1
我倾向于返回,Task<string[]>因为这表明您不再返回具有延迟执行的迭代器,即。所有的URL都将被下载。
johnnycardy

73

tl; dr与yield一起实现的迭代器是一个阻塞结构,因此截至目前,wait和yield不兼容。

因为在上进行迭代IEnumerable是一项阻塞操作,所以调用标记为的方法async仍将以阻塞方式执行该方法,因为它必须等待该操作完成。

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

等待着Method混合的含义。您是否要等到Task具有IEnumerable,然后再对其进行迭代阻止?还是您正在尝试等待的每个值IEnumerable

我认为第二个是所需的行为,在这种情况下,现有的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();
}

1
您是否认为下一个C#语言版本可能会增加foreach对您的假设的支持IAsyncEnumerator<T>

1
@Dai我认为它正在筹备中。对于C#8.0 /已经阅读了。
nawfal


40

根据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的答案暗示,调用代码将异步获取并行获得的结果的集合。上面的代码暗示调用代码将以异步方式从流中一个接一个地获得结果。


17

我知道,我来不及的答案,但在这里是可以与这个库可以实现另一种简单的解决方案:
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);
  });
}

5

此功能将从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的异步版本。语言使您可以等待这些元素消耗它们的元素,并让它们返回以产生元素。

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


1

首先,请记住,异步工作尚未完成。在发布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();

如果将方法定义为async,则还可以有一个“标头”(在循环之前)Task<IEnumerable<Task<string>>> DownloadAllUrl。或者,如果您想要“页脚”操作IEnumerable<Task>。例如gist.github.com/1184435
乔纳森·迪金森

1
现在的async东西完成和C#5.0 发布,这可以被更新。
casperOne

我喜欢这个,但是在这种情况下,foreach(await GetUrls()中的var url){yield return GetContent(url)}; 我只发现了一种分裂为两种方法的方法。
Juan Pablo Garcia Coello


-1

此解决方案按预期方式工作。注意该await Task.Run(() => enumerator.MoveNext())部分。

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;
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.