繁体   English   中英

为什么异步执行在`select`中不起作用?

[英]Why doesn't async execution work in `select`?

我通过AJAX调用此操作(ASP.Net Core 2.0):

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

但是如果我在posts数组中有很多post对象(例如30个post),那么响应时间太长(20秒!!!)。
这是因为此行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对象都一起执行日志记录方法。 换句话说,似乎每个帖子都在await直到上一个帖子完成执行此部分为止:

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

然后,当每个帖子完成该部分时,将对所有post对象执行日志的位置1。

对于日志记录位置2 var post = await dbContext.Pos...发生同样的情况,每个帖子似乎都在等待上一个帖子完成执行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 x30。这意味着它是EF特有的。

为什么只有实体框架(具有原始功能)才会出现此问题? 我注意到这个问题,甚至只有3个post的对象! 有什么新主意吗?

您所做的推论不一定是正确的。

如果这些方法以非异步方式触发,则您将看到来自一个方法调用的所有日志都到达控制台,而下一个方法调用的控制台日志才到达控制台。 您将看到模式123123123而不是111222333 您看到的是,在发生一些异步批处理之后 ,这三个awaits似乎已同步。 因此看来操作是分阶段进行的。 但为什么?

这可能有两个原因。 首先,调度程序可能会将您的所有任务调度到同一线程,从而使每个任务排队,然后在上一个执行流程完成时进行处理。 由于Select循环之外等待Task.WhenAll ,因此异步方法的所有同步部分都在awaited任何一个Task之前执行,因此会导致在调用该方法后立即调用所有“第一个”日志调用

那么,与其他人稍后同步处理什么呢? 同样的事情正在发生。 一旦所有方法都达到了第一次await ,执行流程就交给调用该方法的任何代码。 在这种情况下,这就是您的Select语句。 但是,所有这些异步操作都在后台进行处理。 这创建了竞争条件。

由于请求/响应时间的变化,是否应该不存在在某些方法的第二次日志之前调用某些方法的第三次日志的机会? 大多数时候,是的。 除非您在方程式中引入了某种“延迟”,否则种族状况将更加可预测。 Console日志记录实际上相当慢,并且也是同步的。 这将导致所有方法在日志记录行处阻塞,直到之前的日志完成为止。 但是,仅凭阻塞本身可能不足以使所有这些日志调用几乎全部同步。 可能还有其他因素在起作用。

看来您正在查询数据库。 由于此操作是IO操作,因此完成操作所需的时间比其他操作(可能包括控制台日志记录)要长得多。 这意味着,尽管查询不是同步的,但在所有查询/请求都已发送之后,因此在每种方法的第二条日志行已执行之后 ,它们很有可能会收到响应。 其余的日志行将最终得到处理,因此属于最后一批。

您的代码正在异步处理。 只是看起来不太像您期望的那样。 异步并不意味着随机顺序。 这仅意味着某些代码流将暂停,直到满足以后的条件为止,从而允许在此期间处理其他代码。 如果条件发生同步,那么您的代码流也会同步。

实际上异步执行是可行的,但是它并不能按您期望的那样工作。 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)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM