Reputation: 38013
Here's a dumbed-down version of what I want to do:
private static int Inc(int input)
{
return input + 1;
}
private static async Task<int> IncAsync(int input)
{
await Task.Delay(200);
return input + 1;
}
private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(Func<TInput, TResult> func, IEnumerable<TInput> values)
{
var tasks = values.Select(value => Task.Run(() => func(value)))
.ToList();
await Task.WhenAll(tasks);
return tasks.Select(t => t.Result);
}
public async void TestAsyncStuff()
{
var numbers = new[] { 1, 2, 3, 4 };
var resultSync = await GetResultsAsync(Inc, numbers); // returns IEnumerable<int>
Console.WriteLine(string.Join(",", resultSync.Select(n => $"{n}")));
// The next line is the important one:
var resultAsync = await GetResultsAsync(IncAsync, numbers); // returns IEnumerable<Task<int>>
}
So basically, GetResultsAsync()
is intended to be a generic method that will get the results of a function for a set of input values. In TestAsyncStuff()
you can see how it would work for calling a synchronous function (Inc()
).
The trouble comes when I want to call an asynchronous function (IncAsync()
). The result I get back is of type IEnumerable<Task<int>>
. I could do a Task.WhenAll()
on that result, and that works:
var tasksAsync = (await GetResultsAsync(IncAsync, numbers)).ToList();
await Task.WhenAll(tasksAsync);
var resultAsync = tasksAsync.Select(t => t.Result);
Console.WriteLine(string.Join(",", resultAsync.Select(n => $"{n}")));
But I'd like to tighten up the code and do the await
inline. It should look something like this:
var resultAsync = await GetResultsAsync(async n => await IncAsync(n), numbers);
But that also returns an IEnumerable<Task<int>>
! I could do this:
var resultAsync = await GetResultsAsync(n => IncAsync(n).GetAwaiter().GetResult(), numbers);
And that works... but from what I've seen, use of Task.GetAwaiter().GetResult()
or Task.Result
is not encouraged.
So what is the correct way to do this?
Upvotes: 2
Views: 131
Reputation: 12370
I'd say that your concern is a stylistic one: you want something that reads better. For your first case consider:
var resultSync= numbers.AsParallel()/*.AsOrdered()*/.Select(Inc);
on the grounds that Plinq already does what you're trying to do: It parallelizes IEnumerables
. For your second case, there's no point in creating Tasks
around Tasks
. The equivalent would be:
var resultAsync = numbers.AsParallel()./*AsOrdered().*/Select(n => IncAsync(n).Result);
but I like Sergey's await Task.WhenAll(numbers.Select(IncAsync))
better.
Perhaps what I really like is a Linq style pair of overloads:
var numbers = Enumerable.Range(1,6);
var resultSync = await Enumerable.Range(1,6).SelectAsync(Inc);
var resultAsync = await Enumerable.Range(1,100).SelectAsync(IncAsync);
Console.WriteLine("sync" + string.Join(",", resultSync));
Console.WriteLine("async" + string.Join(",", resultAsync));
static class IEnumerableTasks
{
public static Task<TResult[]> SelectAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> func)
{
return Task.WhenAll( source.Select(async n => await Task.Run(()=> func(n))));
}
public static Task<TResult[]> SelectAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> func)
{
return Task.WhenAll(source.Select(func));
}
}
static int Inc(int input)
{
Task.Delay(1000).Wait();
return input+1;
}
static async Task<int> IncAsync(int input)
{
await Task.Delay(1000);
return input + 1;
}
Which, incidentally, if you change Range(1,6)
to Range(1,40)
shows the advantage of async. On my machine, the timing for the sync can rise steeply where the async version stays at a second or so even for Range(1, 100000)
Upvotes: 0
Reputation: 236248
You should create two overloads of GetResultsAsync
. One should accept a 'synchronous' delegate which returns TResult
. This method will wrap each delegate into a task, and run them asynchronously:
private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(
Func<TInput, TResult> func, IEnumerable<TInput> values)
{
var tasks = values.Select(value => Task.Run(() => func(value)));
return await Task.WhenAll(tasks);
}
The second overload will accept an 'asynchronous' delegate, which returns Task<TResult>
. This method doesn't need to wrap each delegate into a task, because they are already tasks:
private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(
Func<TInput, Task<TResult>> func, IEnumerable<TInput> values)
{
var tasks = values.Select(value => func(value));
return await Task.WhenAll(tasks);
}
You even can call the second method from the first one to avoid code duplication:
private static async Task<IEnumerable<TResult>> GetResultsAsync<TInput, TResult>(
Func<TInput, TResult> func, IEnumerable<TInput> values)
{
return await GetResultsAsync(x => Task.Run(() => func(x)), values);
}
NOTE: These methods don't simplify your life a lot. The same results can be achieved with
var resultSync = await Task.WhenAll(numbers.Select(x => Task.Run(() => Inc(x))));
var resultAsync = await Task.WhenAll(numbers.Select(IncAsync));
Upvotes: 3