在没有Task.WhenAll的情况下并行运行异步操作

时间:2019-07-29 12:24:52

标签: c# .net async-await task-parallel-library .net-4.5

我需要并行运行三个异步I / O操作,尤其是数据库调用。因此,我编写了以下代码:

// I need to know whether these tasks started running here
var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

// await the results
var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;

GetThingOneAsync(), GetThingTwoAsync(), GetThingThreeAsync()方法彼此非常相似,只不过它们具有不同的返回类型(Task<string>, Task<int>, Task<IEnumerable<int>>)。以下是其中一个数据库调用的示例:

public async Task<IEnumerable<int>> GetThingOneAsync(string code)
{
    return await db.DrType.Where(t => t.Code == code).Select(t => t.IdType).ToListAsync();
}

在调试模式下,我可以看到var task1 = _repo.GetThingOneAsync();开始运行GetThingOneAsync()异步方法(与其他两个任务相同)。

我的同事说_repo.GetThingOneAsync()不会启动异步操作。他们说操作是在到达await时开始的(对于我来说,这种说法似乎是错误的)。

因此,他们建议将我的代码修复为以下内容:

var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

await Task.WhenAll(task1, task2, task3);

// then get the result of each task through `Result` property.

在我看来,它与我在问题开始时写的相同,只是Task.WhenAll 如果先前的任务出现故障,则等待以后的任务完成(这是Servy的评论来自this question

我知道我的问题有点重复,但是我想知道我在做对还是错。

2 个答案:

答案 0 :(得分:9)

  

我的同事说_repo.GetThingOneAsync()不会启动异步操作。他们说手术是在我们到达等待状态时开始的(那句话对我来说似乎是错误的)。

他们错了。该方法在调用该方法时开始。

这很容易通过启动具有一些可观察到的副作用(例如写入数据库)的操作来证明。调用该方法,然后例如使用Console.ReadKey()阻止该应用程序。然后,您将看到操作完成(在数据库中)而没有await

剩下的问题全部与风格偏好有关。这些选项之间在语义上略有差异,但通常并不重要。

var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

// await the results
var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;

上面的代码将await(异步等待)一次完成一个任务。如果全部三个都成功完成,那么此代码等效于Task.WhenAll方法。区别在于,如果task1task2有异常,则永远不会等待task3

这是我个人的最爱:

var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

await Task.WhenAll(task1, task2, task3);

var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;

我喜欢Task.WhenAll,因为它很明显。阅读代码后,很明显它正在执行异步并发,因为那里有一个Task.WhenAll

我喜欢使用await而不是Result,因为它对代码更改更具弹性。特别是,如果某人像Task.WhenAll那样不喜欢而将其删除,那么您仍然会await执行这些任务,而不是使用Result,可能导致死锁并在AggregateException中包装异常。 ResultTask.WhenAll之后起作用的唯一原因是这些任务已经完成 并且已经观察到它们的异常。

但这主要是意见。

答案 1 :(得分:0)

我同意@MickyD,这些任务是在初始调用时创建的。这两个调用实际上是相似的。

尽管有些细微差别。当您调用GetThingOneAsync方法时,它会一直执行到到达await语句为止。那就是当它返回任务。如果Async方法从不进行等待,则它将退出并返回一个已完成的Task。因此,如果这些是计算密集型例程(看起来不像),那么您将无法实现任何并行性。您将需要使用Task.Run来实现同时执行。另一点是,如果您从UI线程使用await,则所有执行都将在UI线程上进行-只是在不同的时间进行调度。如果Task正在执行IO,这在某种程度上还可以,因为它将阻塞读取/写入操作。但是它可能会开始累加,因此,如果您要执行任何实质性的操作,则应将其放在线程池中(即使用Task.Run)。

关于您同事的评论,就像我说的,任务1,2,3确实在等待之前开始运行。但是,当您单击等待时,当前正在执行的方法将暂停并返回Task。因此,创建任务的等待机制是正确的-只是您在问题中考虑的任务(任务1、2、3)是GetThingXxxAsync命中等待时创建的任务,而不是您在问题中等待时创建的任务主例程等待任务1,2,3。