何时是使用Task.Result而不是等待Task的最佳位置

Geo*_*ell 7 .net c# asynchronous async-await

虽然我已经在.NET中使用异步代码了一段时间,但我最近才开始研究它并了解正在发生的事情.我刚刚完成了我的代码并试图改变它,所以如果一项任务可以与某些工作并行完成,那么它就是.例如:

var user = await _userRepo.GetByUsername(User.Identity.Name);

//Some minor work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

return user;
Run Code Online (Sandbox Code Playgroud)

现在变成:

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(userTask.Result, DateTime.Now);

return user;
Run Code Online (Sandbox Code Playgroud)

我的理解是现在正在从数据库中获取用户对象WHILST正在进行一些不相关的工作.但是,我看到的帖子暗示结果应该很少使用,等待是首选但我不明白为什么我要等待我的用户对象被提取,如果我可以执行一些其他独立的逻辑同时?

Eri*_*ert 25

让我们确保不要把这里的地方埋没:

所以例如:[一些正确的代码]变成[一些不正确的代码]

绝不能永远不要这样做.

您可以重构控制流以提高性能的直觉非常好且正确.使用Result这样做的错误是错误的.

重写代码的正确方法是

var userTask = _userRepo.GetByUsername(User.Identity.Name);    
//Some work that doesn't rely on the user object    
user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);    
return user;
Run Code Online (Sandbox Code Playgroud)

请记住,await不会使调用异步.等待只是意味着"如果此任务的结果尚未可用,请执行其他操作并在可用后返回此处".该调用已经是异步的:它返回一个任务.

人们似乎认为它await具有共同呼唤的语义; 它不是.相反,await是任务comonad上的提取操作 ; 它是任务的操作员,而不是调用表达式.您通常只是在方法调用上看到它,因为它是将异步操作抽象为方法的常见模式. 返回的任务是等待的事情,而不是呼叫.

但是,我看到的帖子暗示结果应该很少使用,等待是首选但我不明白为什么我想要等待我的用户对象被提取,如果我可以执行一些其他独立的逻辑同时?

为什么你认为使用Result将允许你同时执行其他独立的逻辑??? 结果会阻止您完全这样做.结果是同步等待.在同步等待任务完成时,您的线程无法执行任何其他工作.使用异步等待来提高效率.请记住,await只是意味着"在完成此任务之前,此工作流程无法继续进展,因此如果不完整,请找到更多工作要做,稍后再回来".await正如您所指出的那样,太早可能导致工作效率低下,因为即使任务未完成,工作流也有可能进展.

通过各种方式,在等待发生的地方移动,以提高工作流程的效率,但绝不会永远不会改变它们Result.如果您认为使用Result将永远提高工作流中的并行性效率,那么您对异步工作流的工作原理有一些深刻的误解.检查你的信念,看看你是否能弄清楚哪一个给你这种不正确的直觉.

您必须永远不要使用Result这样的原因不仅仅是因为当您正在进行异步工作流时同步等待效率低下. 它最终会挂起你的过程.请考虑以下工作流程:

  • task1表示将被安排在将来在此线程上执行的作业并生成结果.
  • 异步函数Foo等待task1.
  • task1尚未完成,因此Foo返回,允许此线程运行更多工作.Foo返回表示其工作流程的任务,并在完成时注册完成该任务task1.
  • 该线程现在可以在将来自由工作,包括task1.
  • task1完成,触发执行完工作流程Foo,并最终完成代表工作流程的任务Foo.

现在假设Foo,而不是获取Resulttask1.怎么了? Foo同步等待task1完成,等待当前线程变为可用,这种情况从未发生,因为我们处于同步等待状态. 如果任务以某种方式与当前线程关联,则调用Result会导致线程自身死锁.你现在可以做出没有锁的死锁,只有一个线程!不要这样做.

  • @GeorgeHarnwell:太好了。现在考虑:`Foo(DateTime.Now, await userTask);` 和 `var user = await userTask; 之间的区别是什么?Foo(DateTime.Now, user);` ? 你明白为什么你可能更喜欢一种吗? (2认同)
  • @GeorgeHarnwell:如果等待长时间运行的异步任务需要,哦,比方说三分钟,会发生什么? (2认同)

Har*_*lse 7

异步等待并不意味着多个线程将运行您的代码。

但是,它会减少您的线程等待进程完成的空闲时间,从而提前完成。

每当线程通常必须空闲等待某事完成时,例如等待网页下载、数据库查询完成、磁盘写入完成,async-await 线程将不会空闲等待直到数据写入/ fetched, 但环顾四周是否可以做其他事情,并在等待任务完成后返回。

这在与 Eric Lippert 的采访中用厨师类比进行了描述。在中间的某个地方搜索异步等待。

Eric Lippert 将 async-await 与一个(!)必须做早餐的厨师进行了比较。开始烤面包后,他可以等面包烤好再放茶壶,等水烧开再将茶叶放入茶壶等。

一个异步等待的厨师,不会等待烤面包,而是放在水壶上,当水加热时,他会将茶叶放入茶壶中。

每当厨师不得不无所事事地等待某事时,他就会环顾四周,看看是否可以做其他事情。

异步函数中的线程会做类似的事情。因为函数是异步的,所以你知道函数中有一个 await。事实上,如果你忘记编写 await,你的编译器会警告你。

当你的线程遇到 await 时,它会向上调用它的调用堆栈,看看它是否可以做其他事情,直到它看到一个等待,再次向上调用调用堆栈,等等。一旦每个人都在等待,他就会向下调用堆栈并开始等待,直到第一个可等待的进程完成。

等待进程完成后,线程将继续处理等待之后的语句,直到他再次看到等待。

另一个线程可能会继续处理等待之后的语句(您可以通过检查线程 ID 在调试器中看到这一点)。然而,另一个线程具有原始线程的上下文,因此它可以像原始线程一样运行。不需要互斥体、信号量、IsInvokeRequired(在 winforms 中)等。对您来说,似乎只有一个线程。

有时,您的厨师必须做一些需要花费一些时间的事情,而不能无所事事地等待,例如切西红柿。在这种情况下,聘请不同的厨师并命令他进行切片可能是明智之举。同时,您的厨师可以继续处理刚刚煮沸并需要去皮的鸡蛋。

在计算机术语中,如果您有一些大的计算而不等待其他进程,这将是。注意与例如将数据写入磁盘的区别。一旦您的线程命令需要将数据写入磁盘,它通常会等待直到数据被写入。在进行大型计算时,情况并非如此。

你可以雇佣额外的厨师使用 Task.Run

async Task<TimeSpan> CalculateSunSet()
{
    // start fetching sunset data. however don't wait for the result yet
    // you've got better things to do:
    Task<SunsetData> taskFetchData = FetchSunsetData();

    // because you are not awaiting your thread will do the following:
    Location location = FetchLocation();

    // now you need the sunset data, start awaiting for the Task:
    SunsetData sunsetData = await taskFetchData;

    // some big calculations are needed, that take 33 seconds,
    // you want to keep your caller responsive, so start a Task
    // this Task will be run by a different thread:
    ask<DateTime> taskBigCalculations = Taks.Run( () => BigCalculations(sunsetData, location);

    // again no await: you are still free to do other things
    ...
    // before returning you need the result of the big calculations.
    // wait until big calculations are finished, keep caller responsive:
    DateTime result = await taskBigCalculations;
    return result;
}
Run Code Online (Sandbox Code Playgroud)

  • 这个解释帮助我理解了具有长时间运行任务的方法的结构以及如何正确使用等待。 (2认同)

Nin*_*rry 0

你考虑过这个版本吗?

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);

return user;
Run Code Online (Sandbox Code Playgroud)

这将在检索用户时执行“工作”,但它也具有与 task.Result 相同的等待已完成任务await中描述的所有优点。


正如建议的,您还可以使用更明确的版本来检查调试器中的调用结果。

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await userTask;
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

return user;
Run Code Online (Sandbox Code Playgroud)