如何在执行Task.WhenAny时产生返回项

时间:2013-08-17 01:16:16

标签: c# async-await c#-5.0

我的解决方案中有两个项目:WPF项目和类库。

在我的课程库中:

我有一个符号列表:

class Symbol
{
     Identifier Identifier {get;set;}
     List<Quote> HistoricalQuotes {get;set;}
     List<Financial> HistoricalFinancials {get;set;}
}

对于每个符号,我查询金融服务,以使用webrequest检索每个符号的历史财务数据。 (webClient.DownloadStringTaskAsync(URI))

所以这是我的方法:

    public async Task<IEnumerable<Symbol>> GetSymbolsAsync()
    {
        var historicalFinancialTask = new List<Task<HistoricalFinancialResult>>();

        foreach (var symbol in await _listSymbols)
        {
            historicalFinancialTask.Add(GetFinancialsQueryAsync(symbol));
        }

        while (historicalFinancialTask.Count > 0)
        {
            var historicalFinancial = await Task.WhenAny(historicalFinancialTask);
            historicalFinancialTask.Remove(historicalFinancial);

            // the line below doesn't compile, which is understandable because method's return type is a Task of something
            yield return new Symbol(historicalFinancial.Result.Symbol.Identifier, historicalFinancial.Result.Symbol.HistoricalQuotes, historicalFinancial.Result.Data); 
        }
    }

    private async Task<HistoricalFinancialResult> GetFinancialsQueryAsync(Symbol symbol)
    {
        var result = new HistoricalFinancialResult();
        result.Symbol = symbol;
        result.Data = await _financialsQuery.GetFinancialsQuery(symbol.Identifier); // contains some logic like parsing and use WebClient to query asynchronously
        return result;
    }

    private class HistoricalFinancialResult
    {
        public Symbol Symbol { get; set; }
        public IEnumerable<Financial> Data { get; set; }

        // equality members
    }

正如您所看到的,我希望每次下载每个符号的财务历史数据时,都会产生结果,而不是等待我对金融服务的所有调用完成。

在我的WPF中,这就是我想要做的事情:

foreach(var symbol in await _service.GetSymbolsAsync())
{
      SymbolsObservableCollection.Add(symbol);
}

似乎我们不能在异步方法中产生回报,那么我可以使用什么解决方案?除了将我的GetSymbols方法移动到我的WPF项目中。

4 个答案:

答案 0 :(得分:41)

虽然我喜欢TPL Dataflow组件(svick建议您使用),但转移到该系统确实需要大量的承诺 - 它不是您可以添加到现有设计中的东西。如果您正在执行大量CPU密集型数据处理并希望利用许多CPU内核,它可以提供相当大的好处。但要充分利用它是非常重要的。

他的其他建议,使用Rx,可能更容易与现有解决方案集成。 (请参阅original documentation,但有关最新代码,请使用Rx-Main nuget包。或者,如果您想查看来源,请参阅the Rx CodePlex site)它甚至会如果你愿意,调用代码可以继续使用IEnumerable<Symbol> - 您可以将Rx纯粹用作实现细节,[编辑2013/11/09添加:]尽管作为svick已经指出,鉴于你的最终目标,这可能不是一个好主意。

在我向您展示一个例子之前,我想清楚一下我们究竟在做什么。您的示例有一个带有此签名的方法:

public async Task<IEnumerable<Symbol>> GetSymbolsAsync()

返回类型Task<IEnumerable<Symbol>>,基本上是&#34;这是一种产生类型IEnumerable<Symbol>的单个结果的方法,它可能不会立即产生该结果。&#34; < / p>

单一结果位,我认为这会让你感到悲伤,因为那并不是你想要的。 Task<T>(无论T可能是什么)表示单个异步操作。它可能有许多步骤(await的许多用法,如果你将它作为C#async方法实现),但最终会产生一件事。您希望在不同的时间生成多个内容,因此Task<T>不适合。

如果你真的想做你的方法签名所承诺的 - 最终产生一个结果 - 你可以做的一种方法是让你的异步方法构建一个列表,然后在它好的时候产生它准备好了:

// Note: this first example is *not* what you want.
// However, it is what your method's signature promises to do.
public async Task<IEnumerable<Symbol>> GetSymbolsAsync()
{
    var historicalFinancialTask = new List<Task<HistoricalFinancialResult>>();

    foreach (var symbol in await _listSymbols)
    {
        historicalFinancialTask.Add(GetFinancialsQueryAsync(symbol));
    }

    var results = new List<Symbol>();
    while (historicalFinancialTask.Count > 0)
    {
        var historicalFinancial = await Task.WhenAny(historicalFinancialTask);
        historicalFinancialTask.Remove(historicalFinancial);

        results.Add(new Symbol(historicalFinancial.Result.Symbol.Identifier, historicalFinancial.Result.Symbol.HistoricalQuotes, historicalFinancial.Result.Data)); 
    }

    return results;
}

此方法执行其签名所示:它异步生成一系列符号。

但是大概你想创建一个IEnumerable<Symbol>来生成可用的项目,而不是等到它们全部可用。 (否则,您也可以使用WhenAll。)您可以这样做,但yield return不是这样。

简而言之,我认为你想要做的是产生一个异步列表。其中有一种类型:IObservable<T>表达了我认为您希望用Task<IEnumerable<Symbol>>表达的内容:它是一系列项目(就像IEnumerable<T>一样)但是异步的。

通过类比来理解它可能有所帮助:

public Symbol GetSymbol() ...

public Task<Symbol> GetSymbolAsync() ...

作为

public IEnumerable<Symbol> GetSymbols() ...

是:

public IObservable<Symbol> GetSymbolsObservable() ...

(不幸的是,与Task<T>不同,对于所谓的异步序列导向方法,我们没有通用的命名约定。我最后添加了“Observable”###########################################################在这里,但这不是普遍的做法。我当然不会称之为GetSymbolsAsync因为人们希望返回Task。)

换句话说,Task<IEnumerable<T>>说&#34;当我做好并准备好时,我会制作这个系列&#34;而IObservable<T>说:&#34;这是一个集合。当我做好并做好准备时,我会生产每件商品。&#34;

因此,您需要一个返回Symbol个对象序列的方法,其中这些对象是异步生成的。这告诉我们你应该真的回归IObservable<Symbol>。这是一个实现:

// Unlike this first example, this *is* what you want.
public IObservable<Symbol> GetSymbolsRx()
{
    return Observable.Create<Symbol>(async obs =>
    {
        var historicalFinancialTask = new List<Task<HistoricalFinancialResult>>();

        foreach (var symbol in await _listSymbols)
        {
            historicalFinancialTask.Add(GetFinancialsQueryAsync(symbol));
        }

        while (historicalFinancialTask.Count > 0)
        {
            var historicalFinancial = await Task.WhenAny(historicalFinancialTask);
            historicalFinancialTask.Remove(historicalFinancial);

            obs.OnNext(new Symbol(historicalFinancial.Result.Symbol.Identifier, historicalFinancial.Result.Symbol.HistoricalQuotes, historicalFinancial.Result.Data));
        }
    });
}

正如您所看到的,这可以让您写出您希望编写的内容 - 此代码的主体几乎与您的相同。唯一的区别是你使用yield return(没有编译)的地方,这会调用Rx提供的对象上的OnNext方法。

写完之后,您可以轻松地将其打包成IEnumerable<Symbol>([编辑2013/11/29添加:],尽管您可能实际上并不想做这个 - 在答案结尾处看到补充):

public IEnumerable<Symbol> GetSymbols()
{
    return GetSymbolsRx().ToEnumerable();
}

这可能看起来不是异步的,但事实上它确实允许底层代码异步操作。当您调用此方法时,它不会阻止 - 即使执行获取财务信息的基础代码无法立即生成结果,此方法仍将立即返回IEnumerable<Symbol>。当然,如果数据尚不可用,任何试图遍历该集合的代码都将最终阻塞。但至关重要的是,我认为你最初想要实现的目标是什么:

  • 您可以编写一个async方法来完成工作(我的示例中的委托,作为参数传递给Observable.Create<T>,但如果您愿意,可以编写独立的async方法)
  • 仅仅因为要求您开始提取符号而不会阻止调用代码
  • 生成的IEnumerable<Symbol>会在每个项目可用时立即生成

这是有效的,因为Rx的ToEnumerable方法中有一些聪明的代码,它弥合了IEnumerable<T>的同步世界视图与异步生成结果之间的差距。 (换句话说,这确实让你感到失望的是发现C#无法为你做的。)

如果您有点好奇,可以查看来源。可以在https://rx.codeplex.com/SourceControl/latest#Rx.NET/Source/System.Reactive.Linq/Reactive/Linq/Observable/GetEnumerator.cs

找到代码ToEnumerable所依据的代码

[编辑2013/11/29添加:]

svick在评论中指出我错过了一些内容:你的最终目标是将内容放入ObservableCollection<Symbol>。不知怎的,我没有看到那一点。这意味着IEnumerable<T>是错误的方式 - 您希望在项目可用时填充集合,而不是使用foreach循环。所以你只需这样做:

GetSymbolsRx().Subscribe(symbol => SymbolsObservableCollection.Add(symbol));

或类似的东西。这将在集合可用时将项目添加到集合中。

这取决于顺便在UI线程上启动的整个事情。只要是这样,您的异步代码最终应该在UI线程上运行,这意味着当项目添加到集合时,也会在UI线程上发生。但是如果由于某种原因你最终从工作线程启动了东西(或者如果你在任何等待中使用ConfigureAwait,从而打破了与UI线程的连接),你需要安排到处理右线程上Rx流中的项目:

GetSymbolsRx()
    .ObserveOnDispatcher()
    .Subscribe(symbol => SymbolsObservableCollection.Add(symbol));

如果您在执行此操作时处于UI线程中,它将选择当前的调度程序,并确保所有通知都通过它进行。如果您在订阅时已经使用了错误的线程,则可以使用带有调度程序的ObserveOn重载。 (这些要求您引用System.Reactive.Windows.Threading。这些是扩展方法,因此您需要using来包含名称空间,也称为System.Reactive.Windows.Threading)< / p>

答案 1 :(得分:6)

您要求的内容没有多大意义,因为IEnumerable<T>是一个同步界面。换句话说,如果某个项尚不可用,MoveNext()方法必须阻止,则别无选择。

您需要的是IEnumerable<T>的某种异步版本。为此,您可以使用来自TPL dataflow的Rx或(我最喜欢的)块中的IObservable<T>。有了它,你的代码可能看起来像这样(我还将一些变量更改为更好的名称):

public IReceivableSourceBlock<Symbol> GetSymbolsAsync()
{
    var block = new BufferBlock<Symbol>();

    GetSymbolsAsyncCore(block).ContinueWith(
        task => ((IDataflowBlock)block).Fault(task.Exception),
        TaskContinuationOptions.NotOnRanToCompletion);

    return block;
}

private async Task GetSymbolsAsyncCore(ITargetBlock<Symbol> block)
{
    // snip

    while (historicalFinancialTasks.Count > 0)
    {
        var historicalFinancialTask =
            await Task.WhenAny(historicalFinancialTasks);
        historicalFinancialTasks.Remove(historicalFinancialTask);
        var historicalFinancial = historicalFinancialTask.Result;

        var symbol = new Symbol(
            historicalFinancial.Symbol.Identifier,
            historicalFinancial.Symbol.HistoricalQuotes,
            historicalFinancial.Data);

        await block.SendAsync(symbol);
    }
}

用法可能是:

var symbols = _service.GetSymbolsAsync();
while (await symbols.OutputAvailableAsync())
{
    Symbol symbol;
    while (symbols.TryReceive(out symbol))
        SymbolsObservableCollection.Add(symbol);
}

或者:

var symbols = _service.GetSymbolsAsync();
var addToCollectionBlock = new ActionBlock<Symbol>(
   symbol => SymbolsObservableCollection.Add(symbol));
symbols.LinkTo(
   addToCollectionBlock, new DataflowLinkOptions { PropagateCompletion = true });
await symbols.Completion;

答案 2 :(得分:0)

我相信你也无法将async方法作为迭代器方法。这是.NET的限制。看一下使用Task Parallel Library Dataflow,它可用于处理数据,因为它可用。还有Reactive Extensions.

答案 3 :(得分:0)

为什么不这样做:

public async IEnumerable<Task<Symbol>> GetSymbolsAsync()
{
    var historicalFinancialTask = new List<Task<HistoricalFinancialResult>>();

    foreach (var symbol in await _listSymbols)
    {
        historicalFinancialTask.Add(GetFinancialsQueryAsync(symbol));
    }

    while (historicalFinancialTask.Count > 0)
    {
        var historicalFinancial = await Task.WhenAny(historicalFinancialTask);
        historicalFinancialTask.Remove(historicalFinancial);

        yield return new Symbol(historicalFinancial.Result.Symbol.Identifier, historicalFinancial.Result.Symbol.HistoricalQuotes, historicalFinancial.Result.Data); 
    }
}