I'm looking for a good way to run async methods in parallel, without having the burden of keeping my Task objects and getting their results from their task, because I feel this makes for less readable code.
I know I can run two async methods in parallel like this:
var t1 = RunAsync1();
var t2 = RunAsync2();
await Task.WhenAll(t1, t2)
var result1 = t1.Result;
var result2 = t2.Result;
For a website that makes a lot of API calls to external services that have nothing to do with each other, I was trying to make this more readable for my fellow developers and take away some of the burden of keeping the t1
, t2
, ... variables.
I came up with the following, but don't know if this is a good way to handle this. Are there downsides to passing my ModelClass to SetResultOfTask1
and SetResultOfTask2
methods, like possible memory leaks maybe or something else?
public async Task GetResult()
{
var model = new ModelClass();
await ExecuteInParallel(SetResultOfTask1(model), SetResultOfTask2(model));
//I can now proceed knowing that model.AsyncResult1 and model.AsyncResult2 should be filled in.
}
private async Task ExecuteInParallel(params Task[] tasks)
{
return Task.WhenAll(tasks);
}
private async Task SetResultOfTask1(ModelClass m)
{
m.AsyncResult1 = await GetAsync1();
}
private async Task SetResultOfTask2(ModelClass m)
{
m.AsyncResult2 = await GetAsync2();
}
Or is there perhaps a better way to handle this that keeps its readability? Thank you for your thoughts and advice!
UPDATE
A fair point was made that the method ExecuteInParallel
doesn't have any value, so this can be removed for the sake of readability.
My idea was to avoid a method that looks like this :
public async Task<ModelClass> GetModel()
{
var t1 = RunAsync1();
var t2 = RunAsync2();
var t3 = GetProductTypesAsync();
var t4 = GetUserInformationAsync();
await Task.WhenAll(t1, t2, t3, t4);
var model = new ModelClass();
model.Type1 = t1.Result;
model.Type2 = t2.Result;
if (t3.Result == null) {
//do something else
}
foreach (var productType in t4.Result) {
//get some more information per product type
model.ProductTypes.Add(productType)
}
return model;
}
and replace it with a method that is more expressive in what is happening, like this. As an outside programmer, I can just read what's happening to construct the model.
public async Task<ModelClass> GetModel()
{
var model = new ModelClass();
//these four methods have nothing to do with one another, other than their end result must end up in my instance of ModelClass, which is why I got tempted into trying to execute this in parallel.
await GetGeneralInformationAboutTheProduct(model);
await LoadProductTypes(model);
await LoadUserInformation(model);
await LoadShoppingCartInformation(model);
return model;
}
CodePudding user response:
To enable parallel execution of tasks you could write your method like this.
Note: this doesn't mean that the code will execute in parallel, just that it could.
Task execution can start as soon as the call is made, the choice of what task is executed when should not be your concern.
public async Task<ModelClass> GetModel()
{
var t1 = RunAsync1();
var t2 = RunAsync2();
var t3 = GetProductTypesAsync();
var t4 = GetUserInformationAsync();
var model = new ModelClass();
model.Type1 = await t1;
model.Type2 = await t2;
var t3Result = await t3;
if (t3Result == null) {
//do something else
}
foreach (var productType in await t4) {
//get some more information per product type
model.ProductTypes.Add(productType)
}
return model;
}
This has the benefit of being familiar to the C# population and allowing greater concurrency and less exception aggregation. This should also be more flexible with regards to ValueTask<T>
. Additionally, you make no use of the Task.Result
property, which is often regarded with concern and a potential code smell.
CodePudding user response:
I think the original code is just fine, but if you really wanted to you could use tuple return types to do something like this:
public static class MyRun
{
public static async Task<(T1 r1, T2 r2)> WhenAll<T1, T2>(Task<T1> t1, Task<T2> t2)
{
await Task.WhenAll(t1, t2);
return (t1.Result, t2.Result);
}
public static async Task<(T1 r1, T2 r2, T3 r3)> WhenAll<T1, T2, T3>(Task<T1> t1, Task<T2> t2, Task<T3> t3)
{
await Task.WhenAll(t1, t2, t3);
return (t1.Result, t2.Result, t3.Result);
}
// Etc.
}
Note that you'd have to add a new overload for each number of parameters, which is somewhat laborious...
Anyway, after doing that let's suppose you have the following methods:
static async Task<string> task1()
{
await Task.Delay(100);
return "task1";
}
static async Task<int> task2(int value)
{
await Task.Delay(200);
return value * 420;
}
static async Task<double> task3(double value)
{
await Task.Delay(300);
return Math.Sqrt(value);
}
You could call those and get the results in one line like so:
var (r1, r2, r3) = await MyRun.WhenAll(task1(), task2(10), task3(64));
But as I said, I prefer the normal approach.