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

虽然我已经在.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; 

现在变成:

 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; 

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

我们一定不要把这里的地方埋没:

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

绝不能永远不要这样做。

您可以重新调整控制流以提高性能的直觉非常好且正确。 使用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; 

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

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

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

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

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

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

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

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

在您的情况下,您可以使用:

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

或者更清楚地说:

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

你应该触摸的唯一一次。 .Result就是你知道任务已经完成。 这在您尝试避免创建async状态机的某些情况下非常有用,并且您认为任务很可能同步完成(可能使用async情况的本地函数),或者您正在使用回调而不是async / await ,而你在回调中

作为避免状态机的示例:

 ValueTask FetchAndProcess(SomeArgs args) { async ValueTask Awaited(ValueTask task) => SomeOtherProcessing(await task); var task = GetAsyncData(args); if (!task.IsCompletedSuccessfully) return Awaited(task); return new ValueTask(SomeOtherProcessing(task.Result)); } 

这里的要点是, 如果 GetAsyncData返回同步完成的结果,我们完全避免所有的async机制。

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

但是,它会降低线程等待进程完成的时间,从而提前完成。

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

在Eric Lippert的这篇概述中,这已被描述为厨师类比。 在中间某处搜索异步等待。

Eric Lippert将async-await与一位必须做早餐的厨师比较。 在他开始烘烤面包后,他可以闲着等待,直到面包烤​​好,然后放上水壶喝茶,等到水沸腾,然后将茶叶放入茶壶等。

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

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

异步函数中的线程将执行类似的操作。 因为函数是异步的,所以你知道在函数中有一个等待点。 实际上,如果您忘记编写await编程,编译器会发出警告。

当你的线程遇到await时,它会调高其调用堆栈以查看它是否可以执行其他操作,直到它看到await,再次调用堆栈等等。一旦每个人都在等待,他就会调用堆栈并启动等待,直到第一个等待的过程结束。

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

可能是另一个线程将继续处理await之后的语句(您可以通过检查线程ID在调试器中看到这一点)。 但是这个其他线程具有原始线程的上下文 ,因此它可以表现为原始线程。 不需要互斥锁,信号量,IsInvokeRequired(在winforms中)等等。对你来说,似乎有一个线程。

有时候你的厨师必须做一些花费一些时间而不等闲的事情,比如切西红柿。 在这种情况下,聘请一位不同的厨师并命令他做切片可能是明智之举。 与此同时,你的厨师可以继续煮沸并需要去皮的鸡蛋。

在计算机术语中,如果您在不等待其他过程的情况下进行了一些重大计算,那就是 请注意与例如将数据写入磁盘的区别。 一旦你的线程命令数据需要写入磁盘,它通常会等待,直到数据被写入。 进行大计算时不是这种情况。

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

 async Task CalculateSunSet() { // start fetching sunset data. however don't wait for the result yet // you've got better things to do: Task 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 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; } 

你考虑过这个版本吗?

 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; 

这将在检索用户时执行“工作”,但它也具有等待在任务完成的任务中等待的任务的所有优点, 如task.Result?


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

 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;