在 Task.Run 正确使用方式(2)里,研究了为什么不应该在 CPU-bound 方法实现里使用 Task.Run。相反,我们应该使用 Task.Run 调用该方法。
让我们考虑一个更复杂的场景:服务 API 包括获取数据(I/O-bound)和分析处理数据(CPU-bound)两部分。
class MyService
{
public int PredictStockMarket()
{
// I/O-bound 操作
Thread.Sleep(1000);
// CPU-bound 操作
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
// 更多的 I/O-bound 操作
Thread.Sleep(1000);
// 更多的 CPU-bound 操作
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
return 100;
}
}
现在,我们想使用异步代码,所以可以用异步 I/O 替换堵塞 I/O。但我们如何处理 CPU-bound 部分呢?
一个常见的错误是将它们包装到 Task.Run 里
class MyService
{
public async Task<int> PredictStockMarketAsync()
{
// I/O-bound 操作
await Task.Delay(1000);
// CPU-bound 操作
await Task.Run(() =>
{
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
});
// 更多的 I/O-bound 操作
await Task.Delay(1000);
// 更多的 CPU-bound 操作
await Task.Run(() =>
{
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
});
return 100;
}
}
这是异步方法仍然存在着在方法实现里使用 Task.Run 带来的所有问题。
好吧,API 不能是异步的(因为它有 CPU-bound 部分),也不能是同步的(因为我们想使用异步 I/O)。因此,不幸的是,这里没有理想的解决方案。需要明确的是,我们谈论的是一种极其罕见的边缘情况。绝大多数服务要么是异步的,要么是 CPU-bound 的,而不是两者兼而有之。
目前,最好的解决方案是使用异步签名,但要清楚地记录该方法是 CPU-bound 的异步方法,这样它的性质就不会令人惊讶。
class MyService
{
/// <summary>
/// 该异步方法是 CPU-bound!
/// </summary>
public async Task<int> PredictStockMarketAsync()
{
// I/O-bound 操作
await Task.Delay(1000);
// CPU-bound 操作
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
// 更多的 I/O-bound 操作
await Task.Delay(1000);
// 更多的 CPU-bound 操作
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
return 100;
}
}
这允许基于 UI 的客户端正确使用 Task.Run 来调用服务 API,而基于 ASP.NET 客户端将直接调用该方法。
private async void MyButton_Click(object sender, EventArgs e)
{
await Task.Run(() => myService.PredictStockMarketAsync());
}
public class StockMarketController: Controller
{
public async Task<ActionResult> IndexAsync()
{
var result = await myService.PredictStockMarketAsync();
return View(result);
}
}
在具有异步签名的方法中执行 CPU-bound 工作并不理想,但它确实允许每个可能的客户端以对他们最有意义的方式使用服务 API。每个客户端都充分利用自己的线程情况。
总而言之,即使在罕见和复杂的情况下,最好还是在方法调用时使用 Task.Run,而不是在方法实现中使用 Task.Run。