为什么“select”中的异步执行不起作用?

时间:2018-04-11 22:45:59

标签: c# asp.net .net asynchronous .net-core

我通过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);
}

但是如果post中有很多posts个对象,则响应时间太长(20秒!!!)  阵列(例如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对象执行。换句话说,似乎每个帖子await直到上一篇文章完成执行此部分:

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

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

对于日志记录位置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方法一起打印此行“DEBUG NUMBER HERE RETER RETURN”,这意味着所有select方法在进一步之前等待彼此,我该如何预防是什么?

更新2

使用以下方法替换先前的IsPostLikedByUserAsync方法:

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

在异步运行中没有问题,我不得不等待1秒,而不是1 x 30。 这意味着它是EF特有的东西。

为什么问题只发生在实体框架(原始功能)?即使只有3个post个对象,我也注意到了这个问题!有什么新想法吗?

2 个答案:

答案 0 :(得分:0)

您所做的扣除不一定是真的。

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

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

那么之后与他人同步的交易是什么?同样的事情正在发生。一旦所有方法都达到了它们的第一个await,就会将执行流程转换为调用该方法的任何代码。在这种情况下,这是您的Select声明。然而,在幕后,所有这些异步操作都在处理中。这会造成竞争条件。

由于请求/响应时间的变化,是否有可能在另一种方法的第二个日志之前调用某些方法的第三个日志?大多数时候,是的。除了你引入了一种延迟&#34;在等式中,使竞争条件更具可预测性。 Console日志记录实际上非常慢,并且也是同步的。这会导致所有方法在日志记录行中阻塞,直到先前的日志完成为止。但是阻塞本身可能不足以使所有这些日志调用在很少的批量中同步。可能还有另一个因素在起作用。

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

您的代码正在异步处理。它看起来并不像你期望的那样。异步并不意味着随机顺序。它只是意味着一些代码流暂停,直到满足以后的条件,允许同时处理其他代码。如果条件恰好同步,那么代码流也是如此。

答案 1 :(得分:0)

实际上异步执行有效,但它并没有像你期望的那样工作。 Select语句会为所有帖子启动任务,然后它们会同时工作,从而导致您遇到性能问题。

实现预期行为的最佳方法是降低并行度。没有内置工具可以做到这一点,所以我可以提供2个解决方法:

  1. 使用TPL DataFlow库。它由微软开发,但不是很受欢迎。你可以很容易地找到足够的例子。

  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();
    }));
    
  3. 并且不要忘记在可能的情况下使用.ConfigureAwait(false)。