Как получить возвращаемый элемент при выполнении задачи.Метод whenany

у меня есть два проекта в моем решении: проект 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 ответов


хотя мне нравятся компоненты потока данных TPL (которые svick предлагает вам использовать), переход к этой системе требует существенных обязательств - это не то, что вы можете просто добавить к существующему дизайну. Он предлагает значительные преимущества, если вы выполняете большие объемы обработки данных с интенсивным процессором и хотите использовать многие ядра процессора. Но извлечь из этого лучшее-нетривиально.

его другое предложение, используя Rx, может быть проще интегрировать с существующим решением. (См. оригинальная документация, но для последнего кода используйте Rx-Main пакет nuget. Или, если вы хотите посмотреть на источник, см. сайт RX CodePlex) было бы даже возможно, чтобы вызывающий код продолжал использовать IEnumerable<Symbol> если вы хотите - вы можете использовать Rx исключительно как деталь реализации, [изменить 2013/11/09 добавить:] хотя, как указал Свик, это, вероятно, не очень хорошая идея, учитывая ваш конец цель.

прежде чем я покажу вам пример, я хочу быть ясным о том, что именно мы делаем. В вашем примере был метод с этой подписью:

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

этот тип возврата,Task<IEnumerable<Symbol>>, по существу говорит: "Это метод, который производит один результат типа IEnumerable<Symbol>, и он не может произвести тот результат немедленно."

это один результат немного, что я думаю, вызывает у вас горе, потому что это не то, что вы хотите. А Task<T> (что бы ни случилось!--14--> может быть) представляет собой одну асинхронную операцию. Он может иметь много шагов (много применений 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() ...

as

public IEnumerable<Symbol> GetSymbols() ...

- это:

public IObservable<Symbol> GetSymbolsObservable() ...

(к сожалению, в отличие от С Task<T> нет общего соглашения об именах для того, что вызовите асинхронный метод, ориентированный на последовательность. В конце я добавил "наблюдаемый", но это не универсальная практика. Я бы, конечно, не назвал это GetSymbolsAsync потому что люди будут ожидать, что вернуть Task.)

иными словами, Task<IEnumerable<T>> говорит: "Я произведу эту коллекцию, когда буду готов", тогда как IObservable<T> говорит: "вот коллекция. Я представлю каждый предмет, когда буду готов."

Итак, вам нужен метод, который возвращает последовательность 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 (который не компилировался), это вызывает OnNext метод на объекте, поставляемом Rx.

написал это, вы можете легко обернуть это в 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# не смог сделать для вас.)

Если вам интересно, вы можете посмотреть на источник. Код, который лежит, что ToEnumerable does можно найти на https://rx.codeplex.com/SourceControl/latest#Rx.NET/Source/System.Reactive.Linq/Reactive/Linq/Observable/GetEnumerator.cs

[отредактировано 2013/11/29, чтобы добавить:]

Свик указал в комментариях то, что я пропустил: ваша конечная цель-поставить содержание в ObservableCollection<Symbol>. Почему-то я этого не заметил. Это значит IEnumerable<T> - это неправильный путь, чтобы идти, вы хотите, чтобы заполнить коллекцию как элементы становятся доступными, а не с foreach петли. Итак, вы просто сделаете это:

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

или что-то в этом роде. Это добавит элементы в коллекцию по мере их появления.

это зависит от того, что все это запускается в потоке пользовательского интерфейса, кстати. Как долго, как она, твоя асинхронный код должен работать в потоке пользовательского интерфейса, то есть при добавлении элементов в коллекцию это также происходит в потоке пользовательского интерфейса. Но если по какой-то причине вы запускаете вещи из рабочего потока (или если вы должны были использовать ConfigureAwait на любом из ожиданий, тем самым разрывая соединение с потоком пользовательского интерфейса), вам нужно будет организовать обработку элементов из потока Rx в правом потоке:

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

если вы находитесь в потоке пользовательского интерфейса, когда вы это делаете, он подберет текущий диспетчер, и убедитесь, что все уведомления поступают через него. Если вы уже находитесь в неправильном потоке, когда вы приходите Подписаться, Вы можете использовать ObserveOn перегрузка, которая принимает диспетчер. (Они требуют, чтобы у вас была ссылка на System.Reactive.Windows.Threading. И это методы расширения, поэтому вам понадобится using для их содержащего пространства имен, которое также называется System.Reactive.Windows.Threading)


то, что вы просите не имеет смысла, потому что IEnumerable<T> является синхронным интерфейсом. Другими словами, если элемент еще не доступен,MoveNext() метод заблокировать, у него нет другого выбора.

вам нужна какая-то асинхронная версия IEnumerable<T>. Для этого вы можете использовать IObservable<T> от Rx или (мой любимый) блок от поток данных TPL. При этом ваш код может выглядеть так (я также изменил некоторые переменные, чтобы лучше имена):

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;

Я верю, что вы не можете иметь async метод также является методом итератора. Это ограничение .Сеть. Посмотрите на использование Задача Параллельная Библиотека Потока Данных, его можно использовать для обработки данных по мере их поступления. А также Реактивные Расширения.


почему бы не сделать что-то вроде этого:

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