简体   繁体   English

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

[英]Run async operation in parallel without Task.WhenAll

I need to run three async I/O operations in parallel, particularly they are the database calls. 我需要并行运行三个异步I / O操作,尤其是数据库调用。 So, I write the following code: 因此,我编写了以下代码:

// 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;

The GetThingOneAsync(), GetThingTwoAsync(), GetThingThreeAsync() methods are pretty much the similar to each other except that they have different return types( Task<string>, Task<int>, Task<IEnumerable<int>> ). GetThingOneAsync(), GetThingTwoAsync(), GetThingThreeAsync()方法彼此非常相似,只不过它们具有不同的返回类型( Task<string>, Task<int>, Task<IEnumerable<int>> )。 The example of one of the database calls is the following: 下面是数据库调用之一的示例:

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

In debug mode I can see that var task1 = _repo.GetThingOneAsync(); 在调试模式下,我可以看到var task1 = _repo.GetThingOneAsync(); started to run GetThingOneAsync() async method (the same with other two tasks). 开始运行GetThingOneAsync()异步方法(与其他两个任务相同)。

My colleagues say that _repo.GetThingOneAsync() does not start the async operation. 我的同事说_repo.GetThingOneAsync()不会启动异步操作。 They say that the operation started when we reach await (that statement seems to be wrong for me). 他们说手术是在我们到达await时开始的(这对我来说似乎是错误的)。

So, they suggest to fix my code to the following: 因此,他们建议将我的代码修复为以下内容:

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.

In my opinion, it's the same as I wrote in the very beginning of the question except that Task.WhenAll waits for later tasks to finish if an earlier task faults (this is Servy's comment from this question ) 在我看来,它与我在问题的开始处所写的内容相同,只是Task.WhenAll 在较早的任务发生故障时等待稍后的任务完成 (这是Servy在此问题中的评论)

I know that my question is kind of duplicate but I want to know whether I'm doing things right or wrong. 我知道我的问题有点重复,但是我想知道我是对还是错。

My colleagues say that _repo.GetThingOneAsync() does not start the async operation. 我的同事说_repo.GetThingOneAsync()不会启动异步操作。 They say that the operation started when we reach await (that statement seems to be wrong for me). 他们说手术是在我们到达等待状态时开始的(这对我来说似乎是错误的)。

They're wrong. 他们错了。 The operation starts when the method is called. 该方法在调用方法时开始。

This is easy to prove by starting an operation that has some observable side effect (like writing to a database). 通过启动具有一些可观察到的副作用(例如写入数据库)的操作,很容易证明这一点。 Call the method and then block the application, eg, with Console.ReadKey() . 调用该方法,然后使用例如Console.ReadKey()阻止应用程序。 You will then see the operation complete (in the database) without an await . 然后,您将看到操作完成(在数据库中)而无需await

The remainder of the question is all about stylistic preference. 剩下的问题全部与风格偏好有关。 There's a slight semantic difference between these options, but usually it's not important. 这些选项之间在语义上略有不同,但通常并不重要。

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;

The code above will await (asynchronously wait) for each task to complete, one at a time. 上面的代码将await (异步等待)每个任务一次完成。 If all three complete successfully, then this code is equivalent to the Task.WhenAll approach. 如果所有三个都成功完成,则此代码等效于Task.WhenAll方法。 The difference is that if task1 or task2 have an exception, then task3 is never awaited. 区别在于,如果task1task2具有异常,则永不等待task3

This is my personal favorite: 这是我个人的最爱:

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;

I like the Task.WhenAll because it's explicit. 我喜欢Task.WhenAll因为它很明显。 Reading the code, it's clear that it's doing asynchronous concurrency because there's a Task.WhenAll right there. 阅读代码后,很明显它正在执行异步并发,因为那里有一个Task.WhenAll

I like using await instead of Result because it's more resilient to code changes. 我喜欢使用await而不是Result因为它对代码更改更具弹性。 In particular, if someone who doesn't like Task.WhenAll comes along and removes it, you still end up await ing those tasks instead of using Result , which can cause deadlocks and wrap exceptions in AggregateException . 特别是,如果有人喜欢Task.WhenAll并删除它,那么您仍然要await这些任务,而不是使用Result ,这可能导致死锁并在AggregateException包装异常。 The only reason Result works after Task.WhenAll is because those tasks have already been completed and their exceptions have already been observed. ResultTask.WhenAll之后Task.WhenAll的唯一原因是因为这些任务已经完成并且它们的异常已经被观察到。

But this is largely opinion. 但这在很大程度上是意见。

I agree with @MickyD, the tasks have been created on the initial call. 我同意@MickyD,该任务已在初始调用中创建。 The two calls are similar in effect. 这两个调用实际上是相似的。

A few nuances though. 虽然有些细微差别。 When you call GetThingOneAsync method, it executes up until the point where it reaches an await statement; 当您调用GetThingOneAsync方法时,它会一直执行到到达await语句为止。 that is when it returns the Task. 那就是当它返回任务。 If the Async method never does an await then it exits and returns an already-completed Task. 如果Async方法从不进行等待,则它将退出并返回一个已完成的Task。 So if these were compute-intensive routines (doesn't look like it) then you would not be achieving any parallelism. 因此,如果这些是计算密集型例程(看起来不像),那么您将无法实现任何并行性。 You would need to use Task.Run to achieve simultaneous execution. 您将需要使用Task.Run来实现同时执行。 Another point is that if you use await from the UI thread then all of the execution will be on the UI thread -- just scheduled at different times. 另一点是,如果您从UI线程使用await,则所有执行都将在UI线程上进行-只是在不同的时间进行调度。 This is somewhat OK if the Task is doing IO because it will block for the read/write. 如果Task正在执行IO,这在某种程度上还可以,因为它将阻塞读取/写入操作。 However it can start to add up so if you are going to do anything substantial then you should put it on the thread pool (Ie with Task.Run). 但是,它可能会开始累加,因此,如果您要执行任何实质性的操作,则应将其放在线程池中(即Task.Run)。

As for the comment from your colleagues, as I said, the task1,2,3 do start running before the awaits. 至于您的同事的评论,正如我所说,task1,2,3确实在等待之前开始运行。 But when you hit the await, the method that you are currently executing will suspend and return a Task. 但是,当您单击等待时,当前正在执行的方法将暂停并返回Task。 So it is somewhat correct that it is the await that creates the Task -- just that the task you are thinking about in your question (task1,2,3) is the one created when GetThingXxxAsync hits an await, not the one created when your main routine awaits task1,2,3. 因此,创建任务的等待机制是正确的-只是您在问题中考虑的任务(任务1、2、3)是GetThingXxxAsync命中等待时创建的任务,而不是您在问题中等待时创建的任务主例程等待任务1,2,3。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM