Last time, we looked at CPU-bound methods and learned why we shouldn’t use
Task.Run in the implementation. Rather, we should use it at the point of the call.
Today, let’s consider a more advanced scenario. Instead of a purely CPU-bound method, let’s perform a much more complex operation. We’re going to predict the stock market.
In order to predict the market, our service will need to get some stock quotes from a web service and then perform some very CPU-intensive analysis. During that analysis, the service may need to retrieve more quotes and/or historical data from the web service and then do more calculation.
So this is a much more complex example with both I/O portions and CPU-bound portions. Here’s how the service looks today; it uses synchronous (blocking) I/O calls as well as CPU-intensive analysis.
Now, we want to start taking advantage of asynchronous code, so we can replace our blocking I/O with asynchronous I/O. But what do we do with the CPU-bound portions?
A common mistake is to wrap them in
The problems here are the same problems as last time. We’re still using
Task.Run in the implementation, and we get all the problems that come along with that. It’s still a fake-asynchronous method.
Well, the API can’t be asynchronous (since it has CPU-bound portions) and it can’t be synchronous (since we want to use asynchronous I/O). So, unfortunately there isn’t an ideal solution here. To be clear, we are talking about an extremely rare edge case; the vast majority of services are either asynchronous or CPU-bound, not both.
I’ve discussed this scenario with some Microsoft employees. The Roslyn team in particular has encountered this situation, where some of their operations need to combine heavy file I/O with non-trivial CPU usage. (These conversations occurred before I was an MVP, so this information is not under an NDA).
They concluded that the best solution is to use an asynchronous signature but document the method clearly so that its CPU-bound nature will not be surprising.
This allows UI-based clients to properly use
Task.Run to call the service, while ASP.NET clients would just call the method directly.
Doing CPU-bound work in a method with an asynchronous signature is not ideal, but it does allow every possible client to use the service in the way that makes most sense for them. Each client makes the best use of its own threading situation.
In conclusion, even in the rare and complex cases, it is still best to use
Task.Run at the invocation, not in the implementation.