使用await关键字返回任务和不使用await关键字返回任务的主要区别是什么?

8

我正在封装AspNet.Identity。但是TPL让我感到困惑。

第一个例子:

    public virtual async Task<IdentityResult> RemovePasswordAsync(string userId)
    {
        var user = _store.FindByIdAsync(userId).Result;
        if (user == null)
            throw new InstanceNotFoundException("user");

        user.PasswordHash = String.Empty;
        user.SecurityStamp = String.Empty;
        return await UpdateAsync(user);
    }

    public virtual async Task<IdentityResult> UpdateAsync(TUser user)
    {
        await _store.UpdateAsync(user);
        return new IdentityResult();
    }

第二个例子:
    public virtual Task<IdentityResult> RemovePasswordAsync(string userId)
    {
        var user = _store.FindByIdAsync(userId).Result;
        if (user == null)
            throw new InstanceNotFoundException("user");

        user.PasswordHash = String.Empty;
        user.SecurityStamp = String.Empty;
        return UpdateAsync(user);
    }

    public virtual async Task<IdentityResult> UpdateAsync(TUser user)
    {
        await _store.UpdateAsync(user);
        return new IdentityResult();
    }

客户端将这样进行调用:
    result = await _userManager.RemovePasswordAsync(user.Id);

我的第一个问题是:

当客户端调用第二个方法时,工作会从IO线程转移到线程池线程。而当调用RemovePasswordAsync时,它会调用UpdateAsync,其中包含一个await关键字。那么,在那一点上,这个线程池线程是否会转移到另一个线程池线程?还是TPL继续使用相同的线程?

我的第二个问题是:构建这个async方法的第一种实现和第二种实现之间的主要区别是什么?

编辑:

这是UserStore类的更新方法。(_store.UpdateAsync(user))

    public Task UpdateAsync(TUser user)
    {
        if (user == null)
            throw new ArgumentNullException("user");

        return _userService.UpdateAsync(user);
    }

这是UserService类的更新方法

    public Task UpdateAsync(TUser user)
    {
        return Task.Factory.StartNew(() => Update(user));
    }

2
第二个问题:https://dev59.com/0WEi5IYBdhLWcg3wsODc - noseratio - open to work
3
在Web应用程序中,实际上不应该使用StartNew。原因在于,当你使用await时,执行线程会被发送回CLR线程池,而当你启动新线程时,会从CLR线程池中拉取一个线程,这在Web应用程序中是适得其反的(这会导致线程饥饿现象,IIS无法将新请求分配给线程,因此HTTP.Sys(http堆栈)输入队列将填满,用户将开始被拒绝)。 - Steve's a D
只是为了更清楚,你会遇到线程饥饿问题,因为你的长时间运行的任务仍在使用CLR线程池。 - Steve's a D
2
@Steve 你说得对,我一直以为ASP.NET使用自己的线程集而不是线程池。现在知道了,避免这样切换线程是完全有道理的,因为这只会增加开销。对于那些感兴趣的人,可以参考Stephen Cleary的Task.Run Etiquette Examples: Don't Use Task.Run in the Implementation - dcastro
1
还有这个:https://dev59.com/f3M_5IYBdhLWcg3wjj-r#2642789 - dcastro
显示剩余4条评论
2个回答

3

我来回答你的第一个问题。

你误解了async/await的工作原理。

一个async方法会同步运行,至少会执行到第一个await语句。

当它遇到await时,有两个选择:

  • 如果可等待对象(例如Task)已经完成,执行将继续当前上下文(即UI线程或ASP.NET请求的上下文)。
  • 如果可等待对象尚未完成,它将包装方法的其余部分并安排在任务完成时在当前上下文(即UI线程)上执行(*)。

根据这个定义,你的整个代码将在同一个ASP.NET请求的上下文中运行。

_store.UpdateAsync可能会产生一个线程池线程(例如,通过使用Task.Run)。

更新

根据你更新后的答案,Update(user)将在线程池线程上运行。其他所有内容都将在当前上下文中运行。


(*) 如果没有同步上下文(即控制台应用程序),方法的其余部分将被安排在线程池线程上运行。


是的,我认为你是正确的,_store.UpdateAsync会导致它转移到另一个线程。但是考虑一种方法,在该方法开始时,我只需像这样调用异步方法: var removePasswordTask = RemovePasswordAsync(user.Id); //...... //...... var result = await removePasswordTask;由于此任务是热的,因此在将其委派给任务变量的那一刻就会运行。因此,从我委派它到我等待它的那一刻,执行发生在哪里? - Kemâl Gültekin
1
@KemalGültekin 请看这里的第一个陷阱:C#和F#中的异步陷阱 - dcastro
1
@KemalGültekin即使您不等待RemovePasswordAsync返回的任务,调用也会同步运行,直到它遇到Task.Factory.StartNew。然后,Update(user)被卸载到线程池,并返回代表此作业的Task。该任务一直上升,直到达到最高级别方法,然后执行//...//...。那么,顶级方法将等待此任务 - 您方法的其余部分(即,在await之后发生的任何内容)将在稍后安排在当前上下文中运行。 - dcastro
2
注意,如果您这样做,您将实现并行化。您将同时执行 Update(user)//...。但是,您将“以可扩展性(您可以为多少客户提供服务)换取性能(您可以快速返回响应给单个客户端的速度)”(摘自 Brad Wilson)。 - dcastro

2
我的第二个问题是; 构建此异步方法的第一种实现和第二种实现的主要区别是什么?
您的第一个实现可以通过将阻塞的 `_store.FindByIdAsync(userId).Result` 替换为异步的 `await _store.FindByIdAsync(userId)` 来改进并且应该这样做。
public virtual async Task<IdentityResult> RemovePasswordAsync(string userId)
{
    var user = await _store.FindByIdAsync(userId);
    if (user == null)
        throw new InstanceNotFoundException("user");

    user.PasswordHash = String.Empty;
    user.SecurityStamp = String.Empty;
    return await UpdateAsync(user);
}

如果没有这样的更新,也许Eric Lippert在这里描述了最好的区别。其中一个特定的问题是异常如何可能被抛出和处理

已更新以解决评论问题。在ASP.NET中,您不应该使用Task.Factory.StartNewTask.Run进行卸载。这不是需要保持UI响应的UI应用程序。所有这些只会增加线程切换的开销。调用Task.Run然后等待或阻塞的HTTP请求处理程序将至少需要相同数量的线程才能完成,您不会提高可伸缩性,只会损害性能。另一方面,使用自然IO绑定任务是有意义的,这些任务在挂起时不使用线程。


你说得对,如果没有 await,由于重载,该方法不应标记为 async。但我有另一个问题;当我等待该任务时,它会继续同步运行,直到出现 Task.Run 或 Factory.StartNew 等语句。因此,在那之前,它使用的是同一线程,但在那一点上,它会转移到另一个线程,直到 await 语句,此时返回到调用线程。因此,在这个特定的例子中,是否最好删除 async 关键字,并调用 FindByIdAsync.Result 来阻塞,最后使用 Task.FromResult 返回任务? - Kemâl Gültekin
在这种情况下,.Result不会阻塞,因为结果立即可用,因为整个过程是同步运行的。因此,您可以完全删除任务。 - dcastro
@dcastro,是的,你说得对,但是我仍然需要使用任务来满足接口。 - Kemâl Gültekin
@KemalGültekin,你不能改变接口吗?你是被迫这样做的吗? - dcastro
@dcastro 我已经从Microsoft的UserManager实现中提取了接口,并且我希望我的库与其保持一致。我可以创建重载,但这会使客户端感到困惑(异步还是同步?)。另外,aspnet identity的接口强制我实现异步方法,在异步方法中使用同步内容不好。因此,如果我要将其更改为同步,我必须在每个标识接口的每个方法中实现同步重载。实际上,我认为我们回到了你说的话题,即在这种情况下完全应避免使用异步等待。 - Kemâl Gültekin
1
@KemalGültekin,您应该避免这种情况的发生。但是,如果您不能更改接口,则可以返回 Task.FromResult(Update(user)),但不要使用 .Result。尽管在这种情况下在“同步任务”上阻塞是完全可以的,但是如果实现发生变化并决定使用 Task.Run 而不是 Task.FromResult,客户端可能会遇到死锁问题(常见问题请参见此处)。因此,总之,我建议使用 Task.FromResult 并在顶层使用 await 任务。 - dcastro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接