Почему мой асинхронный метод не вызывается немедленно в C#

Звонивший:

var snapshotMessages = snapshotRepository.GetMessages();
_ = Console.Error.WriteLineAsync("Loading XML timetables...");
// some lengthy operation which loads a large dataset from a SQL database
await foreach (var item in snapshotMessages) {
    // process the item
}

Вызываемый:

    public async IAsyncEnumerable<Pport> GetMessages() {
        Console.Error.WriteLine("Start getting messages");
        var timestamp = DateTime.MinValue;
        // some code which start downloading a large file from FTP
    }

Я хочу распараллелить загрузку базы данных и загрузку. Однако строка «Начать получать сообщения» не появляется, что указывает на то, что программа не ведет себя параллельно, как я ожидал.

В документации сказано, что:

Асинхронный метод выполняется синхронно до тех пор, пока не достигнет своего первого выражения ожидания, после чего метод приостанавливается до завершения ожидаемой задачи. Тем временем управление возвращается вызывающему методу, как показано в примере в следующем разделе.

что здесь не похоже на правду. Что я сделал не так?

Это ведет себя так же, как и неасинхронный блок итератора: пока вы не начнете итерацию, код не запустится.

Jon Skeet 21.08.2024 17:38

Я не понимаю. Разве код не должен выполняться до того, как будет возвращена итерация?

Michael Tsang 21.08.2024 17:40

Не тот код, который вы написали в своем методе, нет. Компилятор генерирует метод, который эффективно создает конечный автомат, и запускает код вашего метода на «ленивой» основе при использовании итерируемого объекта. Я настоятельно рекомендую сначала убедиться, что вы понимаете блоки итераторов синхронизации, а затем смотреть блоки асинхронных итераторов.

Jon Skeet 21.08.2024 17:43

Не нужно закрывать этот вопрос, он на самом деле весьма познавательный. Хотя мой первый инстинкт заключается в том, что он использует неправильный инструмент (вероятно, это должен быть Task<IEnumerable<...>>), и после того, как вы узнаете ответ, он кажется очевидным, я бы не обязательно думал, что вызов IAsyncEnumerable не приведет к вызову его неасинхронной части.

Blindy 21.08.2024 18:08

Какова цель _ = Console.Error.WriteLineAsync("Loading XML timetables..."); в отличие от более простого Console.WriteLine("Loading XML timetables...");?

Theodor Zoulias 21.08.2024 18:35
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Итераторы — как синхронные, так и асинхронные — имеют отложенное выполнение — по сути, конечный автомат неактивен до фактического foreach. Однако это можно легко исправить, немного реструктурировав:

public IAsyncEnumerable<Pport> GetMessages()
{
    // note: this is neither "async" nor has "yield"

    Console.Error.WriteLine("Start getting messages");
    var timestamp = DateTime.MinValue;
    return GetMessagesCore(timestamp);
}
private async IAsyncEnumerable<Pport> GetMessagesCore(DateTime timestamp)
{
    // some code which start downloading a large file from FTP
    ... yield etc
} 

Этот подход также часто используется, чтобы разрешить только методу private иметь параметр [EnumeratorCancellation] CancellationToken cancellationToken для использования с WithCancellation().


Однако! Блок асинхронного итератора, как и обычный блок итератора, представляет собой всего лишь один пассивный насос, позволяющий итеративно выбирать последовательность; это не активно параллельная/параллельная машина. Для этого вам, вероятно, понадобится Channel<T>, то есть Channel<Pport> с отдельным исполнителем, инициируемым через Task.Run (или аналогичный), чтобы у вас была активная пара производитель/потребитель с некоторым ограниченным или неограниченным устройством между ними (Channel<T>). Канал имеет API ReadAllAsync(), который отображает канал как IAsyncEnumnable<T>.

Например:

await foreach(var i in new Foo().GetMessages())
{
    Console.WriteLine(i);
}
class Foo
{
    public IAsyncEnumerable<int> GetMessages()
    {
        Console.Error.WriteLine("Start getting messages");
        // use a bounded channel as a buffer of 10 pending items;
        // there is also an unbounded option available
        Channel<int> channel = Channel.CreateBounded<int>(
            new BoundedChannelOptions(capacity: 10));
        _ = Task.Run(() => Produce(channel.Writer));
        return channel.Reader.ReadAllAsync();
    }

    private async Task Produce(ChannelWriter<int> writer)
    {
        try
        {
            for (int i = 0; i < 1000; i++)
            {
                await writer.WriteAsync(i);
            }
            writer.TryComplete();
        }
        catch (Exception ex)
        {
            writer.TryComplete(ex);
        }
    }
}

Ваше «исправление» не решило проблему. Он просто изменил ведение журнала так, что журналы показывают, что код работает так, как задумано, хотя это не так. ОП хочет, чтобы их длительная операция выполнялась одновременно с их методом GetMessagesCore, что при этом не изменится.

Servy 21.08.2024 17:48

@Servy, это зависит от вашей точки зрения; некоторый код теперь запускается немедленно - но да, если ОП хочет полностью активный насос, это что-то другое - я уточню

Marc Gravell 21.08.2024 17:51

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

Servy 21.08.2024 17:53

@Серви счастливее? (см. редактирование)

Marc Gravell 21.08.2024 17:56

Другие вопросы по теме