使用async await还是task?

9
您构建了一个复杂的计算算法。它需要相当长的时间才能完成,您希望确保您的应用程序保持响应。你该怎么办?
A. 使用async/await。 B. 同步运行代码。 C. 使用Task.Run。 D. 使用BackgroundWorker。
答案是C。然而,有人可以解释一下为什么A是错误的吗?因为问题没有说这个复杂算法是CPU绑定的。如果它是CPU绑定的,那么我们必须使用任务(我不太明白这种推理,但我知道任务确实有助于使当前线程挂起,直到它们完成)。此外,请解释如何决定何时使用async/await和Tasks。

3
虚假二元论;答案是A和C都对。需要注意的是在ASP.NET上,答案是B。 - Stephen Cleary
3个回答

3

你会怎么做?

A. 使用 async/await。

B. 同步运行代码。

C. 使用 Task.Run。

D. 使用 BackgroundWorker。

C# 5.0 中的 async/await 功能是为了让异步代码编写起来“像同步代码一样容易”。如果你查看 WinAPI,你会发现几乎所有的异步端点都会公开一个完全异步的 API,意味着 没有线程。进一步观察,你会注意到这些相同的端点正在执行 I/O bound 操作。

假设您的算法是 CPU 绑定 的,并且编写方式允许在多个处理器上进行有效计算(例如,您的算法没有需要同步的共享状态),则可以利用 .NET 4.0 中引入的 任务并行库。TPL 提供了对 ThreadPool 的抽象,ThreadPool 又试图在多处理器环境中平均分配工作。 await关键字可用于任何可等待对象,这意味着该对象实现了GetAwaiter方法。当您使用可等待对象并希望执行在该可等待对象完成工作后恢复时,应使用awaitTask实现了可等待模式,并用于描述将来将完成的工作单元。当您想要在工作线程上卸载工作单元并且希望在该工作完成之前将控制权返回给调用方法时,可以使用Task.Runawait
另一种将CPU绑定工作分派到多个处理器的方法是使用Parallel类,该类公开了Parallel.ForParallel.ForEach
顺便提一下,如果问题提出者想要将工作卸载到后台线程中,BackgroundWorker也可以使用。自.NET 4.0发布以来,使用TPL是推荐的方法。
总之,使用任务并行库是将工作卸载到后台线程的推荐方式。您可以与Parallel库结合使用,以最大化算法的并行性。在这样做之后,请测试您的代码,以确保使用多个线程的开销不超过同步运行算法所需的时间。 编辑 正如Stephan在评论中提到的那样,您可以将Parallel.ForEachTask.Run组合在一起,将并行循环卸载到后台线程中:
var task = Task.Run(() => Parallel.ForEach(.. //Loop here));

1
将不同的方法结合起来也是很有用的:await Task.Run(() => Parallel.ForEach(...)) 可以在后台线程上并行工作,而 UI 则异步处理。 - Stephen Cleary

2

我认为“计算”意味着在没有其他信息的情况下,CPU绑定的算法。如果算法是IO绑定的,则异步/等待不仅可接受,而且是正确的答案。


事实上,即使您使用 Task.Run(),为了保持UI的响应性,您仍然需要使用 async\await。对吧? - Dimitar Dimitrov
1
没错。当然你可以尝试使用分发器方法。 - Stilgar
1
@DimitarDimitrov,如果你正在使用Task.Run,则传递给它的委托很可能会在与UI线程不同的线程上执行,因此只要你不通过Wait()Result阻塞任务的完成,你的UI将始终保持响应性。然而,如何处理任务的完成并与首次调用Task.Run的线程通信取决于你:你可以使用await,也可以使用具有适当TaskScheduler的任务延续,或者你可以显式捕获原始同步上下文,并从你的委托中进行后续发布。 - Kirill Shlenskiy
2
(续)或者您可以使用Dispatcher.InvokeControl.Invoke,或者您可以自己编写AsyncEnumerator(http://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx)。我的意思是,有大约一百万种方法可以做到这一点,而`async/await`只是其中之一,即使它是每个人脑海中首先想到的东西(这是非常可以理解的)。 - Kirill Shlenskiy
1
@KirillShlenskiy,当然,你是100%正确的,我同意(尽管你有点学究),但问题仍然是垃圾(在我看来)。 - Dimitar Dimitrov
1
@DimitarDimitrov,同意。 - Kirill Shlenskiy

1

我认为这个问题假设您已经编写了一个同步的算法。问题提到它是一个“计算”,模糊地暗示它是CPU密集型的。它也模糊地暗示该算法是同步编写的。例如:

public int CalculateStuff() {
     ....
}

我会考虑创建一个与此方法异步对应的方法:
public async Task<int> CalculateStuffAsync() {
      return await Task.Run(() => CalculateStuff());
}

然后在您的用户界面中:
LoadingIndicator.IsEnabled = true;
ResultTextBox.Text = await CalculateStuffAsync();
LoadingIndicator.IsEnabled = false;

因此,正确的答案可能是A和C。无论如何,这个问题过于模糊,不能得出任何确定的结论。

1
@DimitarDimitrov 同意,除了B之外其他都在技术上是有效的。 - Bas
2
@BasBrekelmans,为 CPU 绑定的工作提供公共的 xxxAsync 包装器是不良设计。这并不意味着调用者不能在需要时将 CalculateStuff 包装在 Task.Run 中。此外,请考虑以下情况:CalculateStuffAsync().Wait()。如果没有使用 ConfigureAwait(false),你刚刚引入了一个死锁 bug,而这个 bug 在编写异步方法之前是不存在的,几乎没有任何好处。 - Kirill Shlenskiy
2
@DimitarDimitrov,不要只听我的话。在安装了非空的SynchronizationContext(即Windows Forms/WPF)的应用程序中运行您的代码片段并查看会发生什么。这是因为您的await需要在异步操作的结果返回之前转换回原始的SynchronizationContext。如果您通过Task.Wait阻塞线程,那么该转换将永远不会发生,因为线程将忙于等待任务(该任务永远不会完成)。棋局结束。去掉async/await,直接返回任务,问题就解决了。 - Kirill Shlenskiy
1
(或者使用ConfigureAwait(false)显然) - Kirill Shlenskiy
1
@DimitarDimitrov,没问题;请接受我的道歉,因为我在回复时把您和Bas Brekelmans搞混了。 - Kirill Shlenskiy
显示剩余3条评论

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