对多个异步和伪造异步方法使用Task.WhenAll

时间:2018-12-18 00:37:52

标签: c# .net asp.net-mvc async-await

我的一位同事重构了我们的控制器方法,以便将所有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);
}

4 个答案:

答案 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()产生单独的线程。但这确实会阻塞主线程。

所以您的选择是:

  1. 启动另一个线程,只是为了让主线程完成其他任务。
  2. 继续使用主线程。如果其他任务需要线程,则必须自己启动线程。

在两种情况下,您一次都使用一个线程(大部分情况下)。在这两种情况下,事务都将在两个后端事务中较慢的一个所需的时间内完成。但是原始选项启动了一个新线程并产生了当前线程。这似乎是多余的工作。

另一方面,如果操作需要两个或多个同步后端事务,则原始选项将更快地完成,因为它们可以并行运行。假设他们支持它。如果后端事务是数据库事务并且使用相同的数据库,则它们很可能相互阻塞,在这种情况下,您仍然看不到任何好处。

答案 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。但是,不要只是命令其他线程不做任何事情就做某事。