Чи можна "чекати повернення доходу 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)
    }
}

Але знову ж таки, компілятор повертає помилку: "не вдається завантажити рядок повідомлення з ресурсів".


Ось справжній код програмування в моєму проекті

Це дуже корисно, коли у мене є завдання зі списком, це завдання може завантажити HTML з URL-адреси, і я використовую синтаксис "повернення доходу чекаю завдання", результат я хочу 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-Async NuGet включає "асинхронні нумератори" у комплекті з підтримкою LINQ. Вони використовують IAsyncEnumerator<T>тип, визначений Арном.
Стівен Клірі

@StephenCleary цей пакет видалено. Оскільки це було віком тому і автором rxteam MS, чи знаєте ви, чи потрапили вони в 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 / чакати в цьому випадку не потрібні. Просто видаліть asyncіз методу і зробіть return Task.WhenAllбезпосередньо.
luiscubal

22
Останній рядок можна записати urls.Select(DownloadHtmlAsync)
коротше

1
Це виявилося саме тією проблемою, з якою я стикався (ліниво завантажуючи рядки з серії URI). Дякую.
Джеймс Ко

13
Це насправді не відповідає на питання про те, що Is it possible to await yield? ви щойно знайшли обхід для цього одного випадку використання. Не відповів на загальне запитання.
Бух Бух

1
Я схильний би повернутися, Task<string[]>оскільки це вказувало б на те, що ви більше не повертаєте ітератор із відкладеним виконанням, тобто. всі URL-адреси завантажуються.
johnnycardy

73

tl; dr Ітератори, реалізовані з урожайністю, є блокуючим конструктом, тому на даний момент очікування та вихід не сумісні.

Довгий час, оскільки ітерація над 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();
}

1
Як ви вважаєте, можливо, що наступна версія мови C # додасть foreachпідтримку вашим гіпотетичним IAsyncEnumerator<T>?
Дай

1
@Dai Я думаю, що це дуже сильно працює. для C # 8.0 / Читав про це.
nawfal


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 / пакети / 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 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

Перш за все, пам’ятайте, що матеріал 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

Мені це подобається, але як у такому випадку пропонувати (var url в очікуванні GetUrls ()) {return 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;
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.