Почему асинхронное выполнение не работает в `select`?

Я вызываю это действие (ASP.Net Core 2.0) через AJAX:

[HttpGet]
public async Task<IActionResult> GetPostsOfUser(Guid userId, Guid? categoryId)
{
    var posts = await postService.GetPostsOfUserAsync(userId, categoryId);

    var postVMs = await Task.WhenAll(
        posts.Select(async p => new PostViewModel
        {
            PostId = p.Id,
            PostContent = p.Content,
            PostTitle = p.Title,
            WriterAvatarUri = fileService.GetFileUri(p.Writer.Profile.AvatarId, Url),
            WriterFullName = p.Writer.Profile.FullName,
            WriterId = p.WriterId,
            Liked = await postService.IsPostLikedByUserAsync(p.Id, UserId),// TODO this takes too long!!!!
        }));

    return Json(postVMs);
}

Но для ответа потребовалось слишком много времени (20 секунд !!!), если у меня много объектов post в posts массив (например, 30 сообщений) .
Это вызвано этой строкой await postService.IsPostLikedByUserAsync.

Копаемся в исходном коде этой функции:

public async Task<bool> IsPostLikedByUserAsync(Guid postId, Guid userId)
{
    logger.LogDebug("Place 0 passed!");

    var user = await dbContext.Users
        .SingleOrDefaultAsync(u => u.Id == userId);

    logger.LogDebug("Place 1 passed!");

    var post = await dbContext.Posts
        .SingleOrDefaultAsync(u => u.Id == postId);

    logger.LogDebug("Place 2 passed!");

    if (user == null || post == null)
        return false;

    return post.PostLikes.SingleOrDefault(pl => pl.UserId == userId) != null;
}

Исследования показали, что через несколько секунд ВСЕ "Место 1 прошло!" методы ведения журнала выполняются вместе для каждого объекта post. Другими словами, кажется, что каждое сообщение awaits до завершения предыдущего сообщения выполняет эту часть:

 var user = await dbContext.Users
        .Include(u => u.PostLikes)
        .SingleOrDefaultAsync(u => u.Id == userId);

И затем - когда каждая публикация завершает эту часть - место 1 журнала выполняется для всех объектов post.

То же самое происходит с местом регистрации 2, каждая отдельная публикация, кажется, ожидает завершения предыдущего сообщения, чтобы завершить выполнение var post = await dbContext.Pos..., а затем функция может пойти дальше, чтобы выполнить место журнала 2 (через несколько секунд после журнала 1 ВСЕ журналы 2 появляются вместе).

Значит, у меня здесь нет асинхронного выполнения. Может ли кто-нибудь помочь мне понять и решить эту проблему?

ОБНОВИТЬ:

Немного изменим код, чтобы он выглядел так:

    /// <summary>
    /// Returns all post of a user in a specific category.
    /// If the category is null, then all of that user posts will be returned from all categories
    /// </summary>
    /// <param name = "userId"></param>
    /// <param name = "categoryId"></param>
    /// <returns></returns>
    [Authorize]
    [HttpGet]
    public async Task<IActionResult> GetPostsOfUser(Guid userId, Guid? categoryId)
    {
        var posts = await postService.GetPostsOfUserAsync(userId, categoryId);
        var i = 0;
        var j = 0;
        var postVMs = await Task.WhenAll(
            posts.Select(async p =>
            {
                logger.LogDebug("DEBUG NUMBER HERE BEFORE RETURN: {0}", i++);
                var isLiked = await postService.IsPostLikedByUserAsync(p.Id, UserId);// TODO this takes too long!!!!
                logger.LogDebug("DEBUG NUMBER HERE AFTER RETURN: {0}", j++);
                return new PostViewModel
                {
                    PostId = p.Id,
                    PostContent = p.Content,
                    PostTitle = p.Title,
                    WriterAvatarUri = fileService.GetFileUri(p.Writer.Profile.AvatarId, Url),
                    WriterFullName = p.Writer.Profile.FullName,
                    WriterId = p.WriterId,
                    Liked = isLiked,
                };
            }));

        return Json(postVMs);
    }

Это показывает, что эта строка «НОМЕР ОТЛАДКИ ЗДЕСЬ ПОСЛЕ ВОЗВРАТА» печатается для ВСЕХ методов select вместе, это означает, что ВСЕ методы select ждут друг друга, прежде чем идти дальше, как я могу предотвратить это?

ОБНОВЛЕНИЕ 2

Замена предыдущего метода IsPostLikedByUserAsync на следующий:

public async Task<bool> IsPostLikedByUserAsync(Guid postId, Guid userId)
{
    await Task.Delay(1000);
}

Не обнаружил проблем с асинхронным запуском, мне пришлось ждать только 1 секунду, а не 1 x 30. Это означает, что это что-то особенное для EF.

Почему проблема возникает ТОЛЬКО с фреймворком сущностей (с исходной функцией)? Я замечаю проблему даже с 3 объектами post! Есть новые идеи?

Поскольку ваш метод возвращает логическое значение, вы можете вернуть return post.PostLikes.Any(pl => pl.UserId == userId);

koryakinp 12.04.2018 01:50

@koryakinp, не могли бы вы объяснить? на самом деле эта строка не должна быть проблемой (как я думаю).

Mohammed Noureldin 12.04.2018 01:54

@MohammedNoureldin надеюсь, что этот ссылка на сайт пригодится. Может быть, я не замечаю, но я не вижу ошибок. Вы можете просто разделить выбранные задачи на отдельную переменную и после вызова метода WhenAll. хотя это не должно иметь значения, но, возможно, он реагирует по-другому. Я удалю свой пост, извините, я неправильно понял ваш вопрос раньше

H.Mikhaeljan 12.04.2018 02:08

@ H.Mikhaeljan, не могли бы вы предложить правку в ответе?

Mohammed Noureldin 12.04.2018 02:11

Разве вы не можете просто создать один запрос к базе данных, который уже возвращает все необходимые данные (Почтаs и Опубликовать лайки) вместе? Это должен быть простой join таблиц Сообщения, Пользователи и Опубликовать лайки. Выполнение нового запроса к базе данных для каждого сообщения снижает производительность, и асинхронный код не может улучшить его.

Ňuf 12.04.2018 02:30

@ Ňuf, который не может быть проблемой производительности, процессор может обрабатывать более 2 запросов в секунду. Однако не могли бы вы в ответе показать мне ваше предложение?

Mohammed Noureldin 12.04.2018 03:19

Вы проверяли производительность своих запросов индивидуально? Кроме того, предназначен ли ваш репозиторий для одновременной обработки нескольких запросов или он обрабатывает их синхронно и по порядку?

cwharris 12.04.2018 03:40

Похоже, ваша база данных работает медленно при попытке обработать 30 запросов одновременно 3 раза, и что она возвращает все результаты одновременно.

cwharris 12.04.2018 03:46

@MohammedNoureldin: Я не предполагал, что это проблема производительности. Вместо этого я хотел указать, что на самом деле это может быть XY проблема. Вместо написания сложного асинхронного кода, который запрашивает Опубликовать лайки для отдельных Почта (что ужасно неэффективно), вы можете полностью избежать своей проблемы, собрав все данные в одном запросе (если это возможно). Такое решение должно не только более эффективно использовать базу данных, но и упростить написание такого кода.

Ňuf 12.04.2018 05:37

Если вы используете структура сущности, вам не нужно «объединять» таблицы самостоятельно, EF сделает это автоматически с таким запросом: from Post p in dbContext.Posts where p.UserId == userId select new PostViewModel() { PostContent = p.Content, IsLiked = p.PostLikes.Any(v => v.UserId == UserId), ... }

Ňuf 12.04.2018 05:38

@ Ňuf, спасибо, на самом деле речь идет о точном понимании проблемы, а не только о поиске решения. Я до сих пор не мог понять, в чем проблема и почему это происходит.

Mohammed Noureldin 12.04.2018 18:43

@ Ňuf Честно говоря, я не уверен, что ваше объяснение верно, я все еще думаю, что вызов лямбды async в любой функции не будет ожидать для каждой из них, но вернет задачу, которая должна продолжать работать в фоновом режиме . Если я ошибаюсь, дайте мне какое-нибудь возможное решение проблемы?

Mohammed Noureldin 12.04.2018 19:58

@MohammedNoureldin: Я понимаю, что вы задали вопрос, чтобы обучить себя, и не ищете обходного пути, поэтому я написал его как комментарий, а не как ответ. Это правильно, что несколько асинхронных запросов могут одновременно выполняться в фоновом режиме. Но одновременная отправка потенциально сотен запросов - не лучшая идея (база данных в любом случае не сможет обрабатывать их сразу), и вполне вероятно, что какой-то более низкий уровень (EF, поставщик ADO.NET или диспетчер сетевых подключений) ограничит количество одновременных запросы.

Ňuf 13.04.2018 11:00

И независимо от того, насколько успешно вы исправите свой асинхронный код, вы никогда не приблизитесь к производительности одного запроса. База данных может выполнять не только один запрос намного эффективнее, чем набор (простых) запросов, но также позволяет избежать множества циклических обращений к веб-серверу -> база данных -> веб-сервер (каждая из которых добавляет задержку) и будет масштабироваться намного лучше, поскольку количество сообщений будет начать расти дальше. Так что я считаю, что это то, что вы должны учитывать в сценарии Realword. BTW IIRC Entity Framework не поддерживает одновременные запросы в одном DbContext - вы уверены, что не используете его таким образом?

Ňuf 13.04.2018 11:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
14
132
2

Ответы 2

Сделанные вами выводы не обязательно верны.

Если бы эти методы запускались не асинхронно, вы бы увидели, что все журналы от одного вызова метода достигают консоли до того, как журналы консоли следующего вызова метода. Вы увидите образец 123123123 вместо 111222333. Вы видите, что три awaits, кажется, синхронизируют после, происходит некоторая асинхронная пакетная обработка. Таким образом, получается, что операции производятся поэтапно. Но почему?

Это может произойти по двум причинам. Во-первых, планировщик может планировать все ваши задачи в одном потоке, заставляя каждую задачу ставиться в очередь, а затем обрабатывать, когда предыдущий поток выполнения завершен. Поскольку Task.WhenAll ожидает вне шлейфа Select, все синхронные части ваших асинхронных методов выполняются до того, как любой из Task станет awaited, поэтому все "первые" вызовы журнала будут называться сразу после вызова этого метода.

Так в чем же дело, если остальные синхронизируются позже? То же самое происходит. Как только все ваши методы достигают своего первого await, поток выполнения передается любому коду, вызвавшему этот метод. В данном случае это ваше заявление Select. Однако за кулисами обрабатываются все эти асинхронные операции. Это создает состояние гонки.

Разве не должна быть вероятность того, что третий журнал некоторых методов будет вызван перед вторым журналом другого метода из-за различного времени запроса / ответа? В большинстве случаев да. За исключением того, что вы внесли в уравнение своего рода «задержку», сделав состояние гонки более предсказуемым. Запись в журнал Console на самом деле довольно медленная, а также синхронная. Это приводит к тому, что все ваши методы блокируются в строке регистрации до тех пор, пока не будут завершены предыдущие журналы. Но одной блокировки может быть недостаточно, чтобы все эти вызовы журнала синхронизировались небольшими порциями. Здесь может быть еще один фактор.

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

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

Не могли бы вы взглянуть на Обновление 2 в моем вопросе?

Mohammed Noureldin 13.04.2018 02:51

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

Лучший способ добиться ожидаемого поведения - уменьшить степень параллелизма. Для этого нет встроенных инструментов, поэтому я могу предложить два обходных пути:

  1. Используйте библиотеку TPL DataFlow. Он разработан Microsoft, но не пользуется большой популярностью. Однако вы легко можете найти достаточно примеров.

  2. Управляйте параллельными задачами самостоятельно с SemaphoreSlim. Это выглядело бы так:

    semaphore = new SemaphoreSlim(degreeOfParallelism);
    cts = new CancellationTokenSource();
    var postVMs = await Task.WhenAll(
    posts.Select(async p => 
    {
        await semaphore.WaitAsync(cts.Token).ConfigureAwait(false);
        cts.Token.ThrowIfCancellationRequested();
        new PostViewModel
        {
            PostId = p.Id,
            PostContent = p.Content,
            PostTitle = p.Title,
            WriterAvatarUri = fileService.GetFileUri(p.Writer.Profile.AvatarId, Url),
            WriterFullName = p.Writer.Profile.FullName,
            WriterId = p.WriterId,
            Liked = await postService.IsPostLikedByUserAsync(p.Id, UserId),// TODO this takes too long!!!!
        }
        semaphore.Release();
    }));
    

И не забывайте использовать .ConfigureAwait (false), когда это возможно.

Это не может быть степень параллелизма, я могу заметить, что 3 post занимают больше времени, чем 1 post. Разве он не может обрабатывать 3 параллельных запроса? Это не может быть правдой

Mohammed Noureldin 13.04.2018 14:43

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