Можно ли «ждать yield return DoSomethingAsync ()»

108

Совместимы ли обычные блоки итератора (т.е. «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)
    }
}

Но опять же компилятор возвращает ошибку: «не удалось загрузить строку сообщения из ресурсов».


Вот настоящий программный код в моем проекте

Это очень полезно, когда у меня есть задача списка, эта задача может быть загружена HTML с URL-адреса, и я использую синтаксис «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>тип, определенный Арне.
Стивен Клири
@StephenCleary, этот пакет был исключен из списка. Поскольку это было много лет назад и написано командой MS rxteam, знаете ли вы, было ли это добавлено в RX?
JoeBrockhaus,
@JoeBrockhaus: Похоже, он живет как Ix-Async .
Стивен Клири

Ответы:

72

То, что вы описываете, можно выполнить с помощью Task.WhenAllметода. Обратите внимание, как код превращается в простой однострочник. Что происходит, так это то, что каждый отдельный URL-адрес начинает загрузку, а затем WhenAllиспользуется для объединения этих операций в одну, Taskкоторую можно ожидать.

Task<IEnumerable<string>> DownLoadAllUrls(string[] urls)
{
    return Task.WhenAll(from url in urls select DownloadHtmlAsync(url));
}
Брайан Гидеон
источник
10
async / await в этом случае не нужны. Просто удалите 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, являются блокирующей конструкцией, поэтому на данный момент await и 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
@ Дай, я думаю, что это еще не все. для C # 8.0 / Прочитал об этом.
nawfal
@Dai, docs.microsoft.com/en-us/dotnet/csharp/whats-new/… .
Виктор Ярема,
40

В соответствии с новыми функциями C # 8.0 ( ссылка №1 и ссылка №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);
}

Ответ Брайана Гидеона подразумевает, что вызывающий код будет асинхронно получать набор результатов, которые были получены параллельно. Приведенный выше код подразумевает, что вызывающий код будет получать результаты, как из потока, один за другим в асинхронном режиме.

АльбертК
источник
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

Асинхронные потоки

Функция async / await в C # 5.0 позволяет получать (и создавать) асинхронные результаты в виде простого кода без обратных вызовов:

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

Это не так полезно, если вы хотите получать (или производить) непрерывные потоки результатов, например, которые вы можете получить от устройства IoT или облачной службы. Для этого существуют асинхронные потоки.

Мы представляем IAsyncEnumerable, чего и следовало ожидать; асинхронная версия IEnumerable. Язык позволяет вам ждать foreach над ними, чтобы использовать их элементы, и возвращать им return для создания элементов.

async IAsyncEnumerable<int> GetBigResultsAsync()
{
    await foreach (var result in GetResultsAsync())
    {
        if (result > 20) yield return result; 
    }
}
Петтер Петтерссон
источник
1

Прежде всего, имейте в виду, что работа с Async еще не завершена. Команде C # еще предстоит пройти долгий путь до выпуска C # 5.

При этом, я думаю, вы можете захотеть собрать задачи, которые запускаются в 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();
Джон Гитцен
источник
У вас также может быть «заголовок» (перед циклом), если вы определяете свой метод как асинхронный Task<IEnumerable<Task<string>>> DownloadAllUrl. Или, если вам нужны действия «нижнего колонтитула» IEnumerable<Task>. Например, gist.github.com/1184435
Джонатан Дикинсон,
1
Теперь, когда asyncматериал будет закончен и C # 5.0 будет выпущен, это может быть обновлен.
casperOne 05
Мне нравится этот, но как в этом случае foreach (var url in await GetUrls ()) {yield return GetContent (url)}; Я нашел только способ разделения на два метода.
Хуан Пабло Гарсия Коэльо,
-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;
        }
    }
}
Франсуа
источник