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

时间:2017-11-14 11:11:13

标签: c# .net asynchronous async-await

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

let data = [String: Any]()

if let value = data["key"] as? Int {

} else if let value = data["key"] as? Float {

} else if let value = data["key"] as? Double {

}

现在变为:

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;

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

4 个答案:

答案 0 :(得分:24)

让我们确保不要在这里埋葬lede:

  

例如:[某些正确的代码]变成[某些不正确的代码]

永远不要这样做。

您可以重新调整控制流以提高性能的直觉非常好且正确。使用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简单地表示“如果此任务的结果尚未可用,请执行其他操作并在可用后返回此处”。调用已经异步:它返回任务

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

  

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

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

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

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

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

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

答案 1 :(得分:4)

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

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<int> FetchAndProcess(SomeArgs args) {
    async ValueTask<int> Awaited(ValueTask<int> task) => SomeOtherProcessing(await task);
    var task = GetAsyncData(args);
    if (!task.IsCompletedSuccessfully) return Awaited(task);
    return new ValueTask<int>(SomeOtherProcessing(task.Result));
}

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

答案 2 :(得分:3)

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

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

每当线程通常不得不等待一些事情要完成时,比如等待网页下载,数据库查询完成,磁盘写入完成,async-await线程将不会等待,直到数据是写入/获取的,但可以查看是否可以执行其他操作,并在等待完成任务后再返回。

这已在this inverview with Eric Lippert中用厨师类比描述。在中间某处搜索异步等待。

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

异步 ​​- 等待做饭,不会等待烤好的面包,而是放在水壶上,当水加热时,他会把茶叶放在茶壶里。

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

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

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

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

可能是另一个线程将继续处理await之后的语句(您可以通过检查线程ID在调试器中看到这一点)。但是,这个其他线程具有原始线程的 context ,因此它可以表现为原始线程。不需要互斥锁,信号量,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;
}

答案 3 :(得分:0)

你考虑过这个版本吗?

await

这将在检索用户时执行“工作”,但它还具有Await on a completed task same as 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; 的所有优点

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

{{1}}