我的一位同事重构了我们的控制器方法,以便将所有IO操作(包括同步操作)封装在单独的任务中,然后通过Task.WhenAll
并行执行所有这些任务。我绝对可以理解这个想法:我们使用更多的线程,但是我们所有的IO操作(并且我们可以有很多)都以最慢的速度执行,但是我仍然不确定这是否是正确的方法。这是有效的方法还是我遗漏了一些东西?在典型的ASP.Net网站应用程序中使用更多线程的成本是否会明显?
这是一些示例代码
public async Task<ActionResult> Foo() {
var dataATask = _dataARepository.GetDataAsync();
var dataBTask = Task.Run(_dataBRepository.GetData());
await Task.WhenAll(dataATask, dataBTask);
var viewModel = new ViewModel(dataATask.Result, dataBTask.Result);
return View(viewModel);
}
答案 0 :(得分:4)
通常,您的代码还可以-与原始代码相比,它将消耗更多的线程和更多的CPU,但是除非您的站点承受沉重的负担,否则不太可能显着影响整体性能。显然,您需要针对特定负载(包括5-10倍常规流量的某种压力级别负载)自己进行测量。
在Task.Run
中包装同步方法不是最佳实践(请参见Should I expose asynchronous wrappers for synchronous methods?)。只要您为这种情况接受额外的线程以换取这种行为,它就可以为您服务。
如果只剩下一个同步操作,则可以使其保持同步,并在同步步骤结束时等待其余操作,以节省该额外线程:
var dataATask = _dataARepository.GetDataAsync();
var dataBTaskResult = _dataBRepository.GetData();
await Task.WhenAll(dataATask); // or just await dataATask if you have only one.
var viewModel = new ViewModel(dataATask.Result, dataBTaskResult);
答案 1 :(得分:1)
考虑这两种方法。
原文:
public async Task<ActionResult> Foo()
{
var dataATask = _dataARepository.GetDataAsync();
var dataBTask = Task.Run(_dataBRepository.GetData());
await Task.WhenAll(dataATask, dataBTask);
var viewModel = new ViewModel(dataATask.Result, dataBTask.Result);
return View(viewModel);
}
^此版本将创建一个新线程来调用_dataBRepository.GetData()
。附加线程将阻塞,直到调用完成。在等待其他线程完成时,主线程将把控制权交还给ASP.NET管道,在该管道中它可以处理其他一些用户的请求。
不同:
public async Task<ActionResult> Foo()
{
var dataATask = _dataARepository.GetDataAsync();
var dataBResult = _dataBRepository.GetData();
await dataATask;
var viewModel = new ViewModel(dataATask.Result, dataBResult);
return View(viewModel);
}
^此版本不会为dataBRepository.GetData()
产生单独的线程。但这确实会阻塞主线程。
所以您的选择是:
在两种情况下,您一次都使用一个线程(大部分情况下)。在这两种情况下,事务都将在两个后端事务中较慢的一个所需的时间内完成。但是原始选项启动了一个新线程并产生了当前线程。这似乎是多余的工作。
另一方面,如果操作需要两个或多个同步后端事务,则原始选项将更快地完成,因为它们可以并行运行。假设他们支持它。如果后端事务是数据库事务并且使用相同的数据库,则它们很可能相互阻塞,在这种情况下,您仍然看不到任何好处。
答案 2 :(得分:1)
除了Alexei Levenkov mentioned about exposing synchronous wrappers for asynchronous methods之外,在ASP.NET应用程序上使用Task.Run
弊大于利。每个Task.Run
将导致2个线程池调度和上下文切换,毫无益处。
答案 3 :(得分:0)
通常,启动另一个线程来执行一些同步操作是没有用的,如果您所要做的只是等到该线程完成即可。
比较:
async Task<int> ProcessAsync()
{
var task = Task.Run(() => DoSomeHeavyCalculations());
int calculationResult = await task;
return calculationResult;
}
使用
int ProcessAsync()
{
return DoSomeHeavyCalculations();
}
除了异步功能使用更多的功能外,它还会限制该功能的重用:只有异步调用者才能使用它。让您的呼叫者决定他是否要异步呼叫您。如果他除了等待也别无选择,可以让呼叫者决定,等等。
顺便说一句,这正是GetData
的作用:它不会强制像您这样的调用者是异步的,它使您可以自由地将其称为同步或使用Task.Run
进行异步
但是,如果您的函数在繁重的计算过程中还有其他事情要做,那么它可能会很有用:
async Task<int> ProcessAsync()
{
var task = Task.Run(() => DoSomeHeavyCalculations());
// while the calculations are being performed, do something else:
DoSomethingElse();
return await task;
}
在您的示例中:如果只有一个“繁重的计算”,请自己执行。如果有很多繁琐的计算,可以考虑使用Task.Run
。但是,不要只是命令其他线程不做任何事情就做某事。