Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert a dictionary of tasks into a dictionary of results

I have a program that has to process a number of objects and produce an analysis. Each analysis results in a string, and the strings are concatenated to create a report. The report needs the results in a certain order, but I want to analyze each item asynchronously, so I manage this by putting everything into a dictionary, which I can then sort before preparing the final output.

Note: For the sake of this example I will pretend that we are analyzing types from the current assembly, although in my case it's more complicated than that.

The basic pattern (I thought) for doing this would be like this:

var types = myAssembly.GetTypes();
var tasks = types.ToDictionary( key => key, value => AnalyzeType(value) );
//AnalyzeType() is an async method that returns Task<string>.

Now we have a dictionary of hot tasks, which may or may not be finished by the time the dictionary is created, because I didn't await anything.

Now to get the results. How do I do it?

Await

In theory all I have to do is await each task; the result of the await operation is the value itself. But this doesn't convert anything.

var results = tasks.ToDictionary( k => k.key, async v => await v.Value );
Console.WriteLine(results.GetType().FullName);

Output:

System.Collections.Generic.Dictionary'2[[System.Type, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Threading.Tasks.Task'1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

I am baffled... I thought by putting await in front of the Task, c# would convert it to the result. But I still have a dictionary of tasks.

GetResult()

Another approach would be to use this:

var results = tasks.ToDictionary( key => key, value => value.GetAwaiter().GetResult() );
Console.WriteLine(results.GetType().FullName);

Output:

System.Collections.Generic.Dictionary'2[[System.Type, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

So this appears to get me what I wanted, but I had to remove the await keyword, and now I get a warning from the compiler that the method will execute synchronously.

I could add

await Task.WhenAll(tasks.Select( kvp => kvp.Value));

...to yield control until all the tasks have finished running (since it might take a while), so then my overall solution would be:

await Task.WhenAll(tasks.Select( kvp => kvp.Value));
var results = tasks.ToDictionary( key => key, value => value.GetAwaiter().GetResult() );
Console.WriteLine(results.GetType().FullName);

Which I guess works. But seems like this isn't the right way; I am suspicious of calling GetAwaiter().GetResult(), and I'd rather not do the extra WhenAll() step if it isn't needed, and it shouldn't be, since I'm getting the awaiters and results for each and every task individually.

What is the right way to do this? Why didn't the await keyword work in my first example? Do I need to GetResult()? And if I do, is it a good idea to include await Task.WhenAll(), or is it better to simply rely on the GetAwaiter() call (which occurs later anyway)?

Click here for a Fiddle if you'd like to work with it.

Edit (answer):

Thanks Shaun for the correct answer. Here is a generalizable extension method if anyone wants something they can just drop into their code base.

public static async Task<Dictionary<TKey, TResult>> ToResults<TKey,TResult>(this IEnumerable<KeyValuePair<TKey, Task<TResult>>> input)
{
    var pairs = await Task.WhenAll
    (
        input.Select
        ( 
            async pair => new { Key = pair.Key, Value = await pair.Value }
        )
    );
    return pairs.ToDictionary(pair => pair.Key, pair => pair.Value);
}
like image 667
John Wu Avatar asked Feb 05 '23 00:02

John Wu


2 Answers

Why didn't the await keyword work in my first example?

The await keyword unwraps the Task<T> within the context of an async method, operates on the underlying result of type <T>, and wraps the async method's return value back in a Task. That is why every async method/function returns one of void, Task, or Task<T> (note that void is only appropriate for events). An async method does not return an unwrapped value; we never see a method signature like public async int SomeMethod(), because returning int would not compile in an async method. It would need to return a Task<int> instead.

What is the right way to do this?

Here is one approach (with a Fiddle) to converting a dictionary with values of type Task<T> to a dictionary with values of type <T>:

using System.Threading.Tasks; 
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public async static void Main()
    {
        // create a dictionary of 10 tasks
        var tasks = Enumerable.Range(0, 10)
            .ToDictionary(i => i, i => Task.FromResult(i * i));

        // await all their results
        // mapping to a collection of KeyValuePairs
        var pairs = await Task.WhenAll(
            tasks.Select(
                async pair => 
                    new KeyValuePair<int, int>(pair.Key, await pair.Value)));

        var dictionary = pairs.ToDictionary(p => p.Key);

        System.Console.WriteLine(dictionary[2].Value); // 4
    }
}
like image 103
Shaun Luttin Avatar answered Feb 08 '23 17:02

Shaun Luttin


The solution is to get rid of the ToDictionary call and build the dictionary yourself:

var result = new Dictionary<Type, string>();
foreach (var kvp in tasks) 
{ 
    result[kvp.Key] = await kvp.Value;
}

The reason why ToDictionary doesn't work is that you want the value pulled out of the task not another task. Creating an async lambda just creates another task that you then have to await.

like image 36
Mike Zboray Avatar answered Feb 08 '23 17:02

Mike Zboray