async不应该用于高CPU任务吗?

我想知道async是否正确 – await不应该用于“高CPU”任务。 我在演讲中看到了这一点。

所以我猜这意味着什么

 Task calculateMillionthPrimeNumber = CalculateMillionthPrimeNumberAsync(); DoIndependentWork(); int p = await calculateMillionthPrimeNumber; 

我的问题是,上述情况是否合理,或者如果没有,是否还有其他一些使高CPU任务异步的例子?

我想知道async-await是否应该用于“高CPU”任务。

是的,这是真的。

我的问题是上述情况是否合理

我会说这是没有道理的。 在一般情况下,您应该避免使用Task.Run来实现具有异步签名的方法。 不要为同步方法公开异步包装器 。 这是为了防止消费者混淆,特别是在ASP.NET上。

但是,使用Task.Run 调用同步方法(例如,在UI应用程序中)没有任何问题。 通过这种方式,您可以使用multithreading( Task.Run )来保持UI线程的自由,并使用await来优雅地使用它:

 var task = Task.Run(() => CalculateMillionthPrimeNumber()); DoIndependentWork(); var prime = await task; 

事实上,async / await有两个主要用途。 一个(我的理解是,这是它被放入框架的主要原因之一)是使调用线程在等待结果时执行其他工作。 这主要用于I / O绑定任务(即主要“保留”是某种I / O的任务 – 等待硬盘驱动器,服务器,打印机等响应或完成其任务)。

作为旁注,如果你以这种方式使用async / await,重要的是要确保你已经实现了它,使得调用线程在等待结果时可以实际完成其他工作; 我见过很多人做“A等待B,等待C”的事情; 这可能最终表现不比A同时调用B同步而B同步调用C(因为调用线程在等待B和C的结果时从不允许做其他工作)。

在I / O绑定任务的情况下,创建额外的线程只是为了等待结果是没有意义的。 我通常的比喻是考虑在一个有10个人的餐馆中订购。 如果服务员要求订购的第一个人还没有准备好,那么服务员不会在他接受其他任何人的订单之前等待他做好准备,他也不会带第二个服务员等待第一个人。 在这种情况下,最好的办法是询问小组中的其他9个人的订单; 希望,到他们订购时,第一个人就会准备好。 如果没有,至少服务员仍然节省了一些时间,因为他花了更少的时间闲着。

也可以使用Task.Run类的Task.Run来执行CPU绑定任务(这是第二次使用它)。 按照我们上面的类比,这是一个案例,通常有更多的服务员是有用的 – 例如,如果一个服务员有太多的桌子供服务。 真的,这实际上是“幕后”的所有内容都是使用线程池; 它是执行CPU绑定工作的几种可能构造之一(例如,只是将它“直接”放在线程池上,显式创建一个新线程,或者使用后台工作器 )所以这是一个设计问题,你最终会使用哪种机制。

这里async/await一个优点是它可以(在适当的情况下)减少你必须手动编写的显式锁定/同步逻辑的数量。 这是一个愚蠢的例子:

 private static async Task SomeCPUBoundTask() { // Insert actual CPU-bound task here using Task.Run await Task.Delay(100); } public static async Task QueueCPUBoundTasks() { List tasks = new List(); // Queue up however many CPU-bound tasks you want for (int i = 0; i < 10; i++) { // We could just call Task.Run(...) directly here Task task = SomeCPUBoundTask(); tasks.Add(task); } // Wait for all of them to complete // Note that I don't have to write any explicit locking logic here, // I just tell the framework to wait for all of them to complete await Task.WhenAll(tasks); } 

显然,我在这里假设任务是完全可并行化的。 另外请注意,您可以在这里自己使用线程池,但这样会有点不太方便,因为您需要某种方法来弄清楚自己是否所有这些都已完成(而不仅仅是让框架弄明白这一点)为了你)。 您也可以在这里使用Parallel.For循环。

假设您的CalculateMillionthPrimeNumber类似于以下内容(在使用goto非常有效或理想,但非常简单):

 public int CalculateMillionthPrimeNumber() { List primes = new List(1000000){2}; int num = 3; while(primes.Count < 1000000) { foreach(int div in primes) { if ((num / div) * div == num) goto next; } primes.Add(num); next: ++num; } return primes.Last(); } 

现在,这里没有用处可以做异步的事情。 让我们使用async使其成为任务返回方法:

 public async Task CalculateMillionthPrimeNumberAsync() { List primes = new List(1000000){2}; int num = 3; while(primes.Count < 1000000) { foreach(int div in primes) { if ((num / div) * div == num) goto next; } primes.Add(num); next: ++num; } return primes.Last(); } 

编译器会警告我们,因为我们无处可去await任何有用的东西。 真正调用它将与调用Task.FromResult(CalculateMillionthPrimeNumber())稍微复杂一点的版本相同。 也就是说,它与进行计算相同, 然后创建一个已完成的任务,其中计算出的数字作为结果。

现在,已经完成的任务并不总是毫无意义。 例如,考虑:

 public async Task GetInterestingStringAsync() { if (_cachedInterestingString == null) _cachedInterestingString = await GetStringFromWeb(); return _cachedInterestingString; } 

当字符串在缓存中时,这将返回已完成的任务,否则返回,在这种情况下,它将返回非常快。 其他情况是,如果同一接口有多个实现,并且并非所有实现都可以使用异步I / O.

同样, await此方法的async方法将返回已完成的任务,具体取决于此。 这实际上是一种非常好的方式,只需保持相同的线程,并尽可能地做需要的事情。

但是如果它总是可能的话,那么唯一的影响就是创建Task对象和async用来实现它的状态机。

所以,毫无意义。 如果这就是您的问题中的版本实现的方式,那么calculateMillionthPrimeNumber将使IsCompleted从一开始就返回true。 您应该刚刚调用非异步版本。

好的,作为CalculateMillionthPrimeNumberAsync()的实现者,我们想要为我们的用户做一些更有用的事情。 所以我们这样做:

 public Task CalculateMillionthPrimeNumberAsync() { return Task.Factory.StartNew(CalculateMillionthPrimeNumber, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } 

好的,现在我们不会浪费用户的时间。 DoIndependentWork()将与CalculateMillionthPrimeNumberAsync() ,如果它首先完成,则await将释放该线程。

大!

只是,我们还没有真正从同步位置移动针。 实际上,特别是如果DoIndependentWork()不是很艰苦,我们可能会让它变得更糟。 同步方式将在一个线程上执行所有操作,我们称之为Thread A 新方法在Thread B上进行计算,然后释放Thread A ,然后以几种可能的方式进行同步。 这是很多工作,它有什么收获吗?

好吧,也许,但是CalculateMillionthPrimeNumberAsync()的作者无法知道,因为影响它的因素都在调用代码中。 调用代码本身可以完成StartNew ,并且能够更好地满足同步选项的需要。

因此,虽然任务可以是一种与另一个任务并行调用cpu绑定代码的便捷方式,但这样做的方法并不实用。 更糟糕的是他们欺骗,因为有人看到CalculateMillionthPrimeNumberAsync可能会被原谅,因为他们认为调用它并非毫无意义。

除非CalculateMillionthPrimeNumberAsync本身不断使用async/await ,否则没有理由不让Task运行繁重的CPU工作,因为它只是将您的方法委托给ThreadPool的线程。

什么是ThreadPool线程,它与常规线程的区别在于此处 。

简而言之,它只需要将线程池线程保留一段时间(并且线程池线程的数量是有限的),因此,除非你占用太多它们,否则没有什么可担心的。