2

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?

klashar
  • 2,519
  • 2
  • 28
  • 38
Shaul Behr
  • 36,951
  • 69
  • 249
  • 387
  • `var resultAsync = await GetResultsAsync(n => IncAsync(n).Result, numbers);`? – René Vogt Feb 06 '17 at 12:15
  • 2
    Don't use `async void`, it's only meant for event handlers. You can't await an `async void` method – Panagiotis Kanavos Feb 06 '17 at 12:15
  • 3
    Also `await Task.WhenAll(tasks); return tasks.Select(t => t.Result);`? Why? If all tasks have a return type, `WhenAll` returns an array of the results. By cleaning up the code you should be able to write `int[] results=await Task.WhenAll(tasks);` – Panagiotis Kanavos Feb 06 '17 at 12:18
  • 2
    What are you trying to do after all? If you end up calling `Task.Run`, why not use `Parallel.For`? `Task.Run` doesn't make anything asynchronous, it runs jobs in the background – Panagiotis Kanavos Feb 06 '17 at 12:19
  • @PanagiotisKanavos the `async void` is only a test method; it's not functional code :-) – Shaul Behr Feb 06 '17 at 12:33
  • @ShaulBehr which means you won't even know when the test finished, the harness will have exited. This is only *one* of the unexpected constructs in the code – Panagiotis Kanavos Feb 06 '17 at 12:34
  • @PanagiotisKanavos I like the idea of changing the code to use `Parallel.For()`. Could you please post an answer with sample code, and an explanation of how it is different from `Task.Run()`? – Shaul Behr Feb 06 '17 at 12:39
  • 1
    `Parallel.For()` is for `Actions`, not `Func`s? I'd say Plinq is for `Funcs` – Chris F Carroll Feb 06 '17 at 13:05

2 Answers2

3

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));
Shaul Behr
  • 36,951
  • 69
  • 249
  • 387
Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
0

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)

Chris F Carroll
  • 11,146
  • 3
  • 53
  • 61
  • This is great. I finally went with a combination of your answer with Sergey's, using `.AsParallel()`, based on the performance considerations mentioned [here](http://stackoverflow.com/a/19103047/7850). Unfortunately I could only give one of you answer credit... :) – Shaul Behr Feb 06 '17 at 13:29