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线程,它与常规线程的区别在于此处 。
简而言之,它只需要将线程池线程保留一段时间(并且线程池线程的数量是有限的),因此,除非你占用太多它们,否则没有什么可担心的。