简体   繁体   English

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

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

I am calling this action (ASP.Net Core 2.0) over AJAX: 我通过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);
}

But it took too long to response (20 seconds!!!) in case I have many post objects in posts array (eg 30 posts). 但是如果我在posts数组中有很多post对象(例如30个post),那么响应时间太长(20秒!!!)。
That is caused because of this line await postService.IsPostLikedByUserAsync . 这是因为此行await postService.IsPostLikedByUserAsync

Digging into the source code of this function: 深入研究此函数的源代码:

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

The investigations showed, after some seconds, ALL "Place 1 passed!" 调查显示,几秒钟后, 所有 “地方1通过!” logging methods are executed together for every post object. 每个post对象都一起执行日志记录方法。 In other words, it seems that every post await s until the previous post finishes executing this part: 换句话说,似乎每个帖子都在await直到上一个帖子完成执行此部分为止:

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

And then -when every post finishes that part- the place 1 of log is executed for all post objects. 然后,当每个帖子完成该部分时,将对所有post对象执行日志的位置1。

The same happens for logging place 2, every single post seems to await for the previous post to finish executing var post = await dbContext.Pos... , and then the function can go further to execute log place 2 (after few seconds from log 1, ALL log 2 appear together). 对于日志记录位置2 var post = await dbContext.Pos...发生同样的情况,每个帖子似乎都在等待上一个帖子完成执行var post = await dbContext.Pos... ,然后该函数可以进一步执行日志位置2(距离日志几秒钟后) 1,所有日志2一起出现)。

That means I have no asynchronous execution here . 这意味着我这里没有异步执行 Could some one help me to understand and solve this problem? 有人可以帮助我理解和解决这一问题吗?

UPDATE: 更新:

Changing the code a bit to look like this: 稍微更改一下代码,如下所示:

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

That shows, that this line "DEBUG NUMBER HERE AFTER RETURN" is printed for ALL select methods together, that means that ALL select methods waits for each other before going further, how can I prevent that? 那表明,这行“返回后调试号在此处”是为所有select方法一起打印的,这意味着所有select方法在进一步操作之前都会互相等待,如何防止这种情况发生?

UPDATE 2 更新2

Substituting the previous IsPostLikedByUserAsync method, with the following one: 用以下方法替换以前的IsPostLikedByUserAsync方法:

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

Showed no problem in async running, I had to wait only 1 second, not 1 x 30. That means it is something specific to EF. 在异步运行中没有问题,我只需要等待1秒钟,而不是1 x30。这意味着它是EF特有的。

Why does the problem happen ONLY with entity framework (with the original function)? 为什么只有实体框架(具有原始功能)才会出现此问题? I notice the problem even with only 3 post objects! 我注意到这个问题,甚至只有3个post的对象! Any new ideas? 有什么新主意吗?

The deductions you've made are not necessarily true. 您所做的推论不一定是正确的。

If these methods were firing in a non-asynchronous fashion, you would see all of the logs from one method invocation reach the console before the next method invocation's console logs. 如果这些方法以非异步方式触发,则您将看到来自一个方法调用的所有日志都到达控制台,而下一个方法调用的控制台日志才到达控制台。 You would see the pattern 123123123 instead of 111222333 . 您将看到模式123123123而不是111222333 What you are seeing is that the three awaits seem to synchronize after some asynchronous batching occurs. 您看到的是,在发生一些异步批处理之后 ,这三个awaits似乎已同步。 Thus it appears that the operations are made in stages. 因此看来操作是分阶段进行的。 But why? 但为什么?

There are a couple reasons this might happen. 这可能有两个原因。 Firstly, the scheduler may be scheduling all of your tasks to the same thread, causing each task to be queued and then processed when the previous execution flow is complete. 首先,调度程序可能会将您的所有任务调度到同一线程,从而使每个任务排队,然后在上一个执行流程完成时进行处理。 Since Task.WhenAll is awaited outside of the Select loop , all synchronous portions of your async methods are executed before any one Task is awaited , therefore causing all of the "first" log invocations to be called immediately following the invocation of that method. 由于Select循环之外等待Task.WhenAll ,因此异步方法的所有同步部分都在awaited任何一个Task之前执行,因此会导致在调用该方法后立即调用所有“第一个”日志调用

So then what's the deal with the others syncing up later? 那么,与其他人稍后同步处理什么呢? The same thing is happening. 同样的事情正在发生。 Once all of your methods hit their first await , the execution flow is yielded to whatever code invoked that method. 一旦所有方法都达到了第一次await ,执行流程就交给调用该方法的任何代码。 In this case, that is your Select statement. 在这种情况下,这就是您的Select语句。 Behind the scenes, however, all of those async operations are processing. 但是,所有这些异步操作都在后台进行处理。 This creates a race condition. 这创建了竞争条件。

Shouldn't there be some chance of the third log of some methods being called before the second log of another method, due to varying request/response times? 由于请求/响应时间的变化,是否应该不存在在某些方法的第二次日志之前调用某些方法的第三次日志的机会? Most of the time, yes. 大多数时候,是的。 Except you've introduced a sort of "delay" in to the equation, making the race condition more predictable. 除非您在方程式中引入了某种“延迟”,否则种族状况将更加可预测。 Console logging is actually quite slow, and is also synchronous. Console日志记录实际上相当慢,并且也是同步的。 This causes all of your methods to block at the logging line until the previous logs have completed. 这将导致所有方法在日志记录行处阻塞,直到之前的日志完成为止。 But blocking, by itself, may not be enough to make all of those log calls sync up in pretty little batches. 但是,仅凭阻塞本身可能不足以使所有这些日志调用几乎全部同步。 There may be another factor at play. 可能还有其他因素在起作用。

It would appear that you are querying a database. 看来您正在查询数据库。 Since this is an IO operation, it takes considerably longer to complete than other operations (including console logging, probably). 由于此操作是IO操作,因此完成操作所需的时间比其他操作(可能包括控制台日志记录)要长得多。 This means that, although the queries aren't synchronous, they will in all likelihood receive a response after all of the queries/requests have already been sent, and therefore after the second log line from each method has already executed. 这意味着,尽管查询不是同步的,但在所有查询/请求都已发送之后,因此在每种方法的第二条日志行已执行之后 ,它们很有可能会收到响应。 The remaining log lines are processed eventually, and therefore fall in to the last batch. 其余的日志行将最终得到处理,因此属于最后一批。

Your code is being processed asynchronously. 您的代码正在异步处理。 It just doesn't look quite how you might expect. 只是看起来不太像您期望的那样。 Async doesn't mean random order. 异步并不意味着随机顺序。 It just means some code flow is paused until a later condition is met, allowing other code to be processed in the mean time. 这仅意味着某些代码流将暂停,直到满足以后的条件为止,从而允许在此期间处理其他代码。 If the conditions happen to sync up, then so does your code flow. 如果条件发生同步,那么您的代码流也会同步。

Actually async execution works, but it doesn't work as you expect. 实际上异步执行是可行的,但是它并不能按您期望的那样工作。 Select statement starts tasks for all posts and then they all work concurrently that leads you to performance problems you. Select语句启动所有帖子的任务,然后它们同时工作,从而导致性能问题。

The best approach to achieve expected behavior is to reduce the degree of parallelism. 实现预期行为的最佳方法是降低并行度。 There are no build-in tools to do that so I can offer 2 workarounds: 没有内置工具可以执行此操作,因此我可以提供两种解决方法:

  1. Use TPL DataFlow library. 使用TPL DataFlow库。 It is developed by Microsoft but not very popular. 它是由Microsoft开发的,但不是很流行。 You can easily find enough examples though. 不过,您可以轻松找到足够的示例。

  2. Manage parallel tasks by yourself with SemaphoreSlim . 使用SemaphoreSlim自己管理并行任务。 It would look like this: 它看起来像这样:

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

And don't forget to use .ConfigureAwait(false) whenever it's possible. 并且请不要忘记使用.ConfigureAwait(false)。

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

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