簡體   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