Fab
Fab

Reputation: 29

Async/Await refactor approach

When we look at the generated IL code we quickly realize that async mechanics is nice but not free.
How to approach the implementation of such a vampiric code ...
Example :

Imagine that the following method (uninteresting i'm ok) is called almost everywhere in an app..

private Dictionary<string,string> _cache = new Dictionary<string,string>();
public string FirstWord(string t)
{
  if(!_cache.TryGetValue(t, out var r))
    cache[t] = r = t[0, t.IndexOf(' ')];
  return r;
}

In time this method will become more expensive with a path as a parameter (it could have been a URL or other).
It should naturally evolve into async such as:

private Dictionary<string,string> _cache = new Dictionary<string,string>();
public async Task<string> FirstWord(string path)
{
  if(!_cache.TryGetValue(path, out var r))
  {
    var t= await Path.ReadAllTextAsync(path);
    cache[path] = r = t[0, t.IndexOf(' ')];
  }
  return r;
}

As this method is used everywhere I am tempted to migrate all of my code in async
This transformation will also force me to do these kinds of things to manage overloads:

public async Task<string> FirstWord()
  => await FirstWord("Default");
public async Task<string> FirstWord(string path)

The overload without parameters will have to implement an async / await logic to just call the other overload (IL add try / catch...) ...

Should this caching be limited by using a GetAwaiter().GetResult() ...?
Do not do async knowing that this approach will require a general transformation can be more expensive?

Your impressions? Your approach?

Thanks for your feedback

Upvotes: 0

Views: 247

Answers (3)

Theodor Zoulias
Theodor Zoulias

Reputation: 43515

Here is a more detailed version of Paulo Morgado's answer. By caching tasks instead of naked values you can afford to elide the async from the FirstWordAsync method, removing any considerations regarding the efficiency of the method's overloads.

private readonly Dictionary<string, Task<string>> _cache
    = new Dictionary<string, Task<string>>();

public Task<string> FirstWordAsync(string path)
{
    if (!_cache.TryGetValue(path, out var task))
    {
        task = Materialize(async () => // alternative: Task.Run(async...
        {
            string text = await File.ReadAllTextAsync(path).ConfigureAwait(false);
            var index = text.IndexOf(" ");
            return index == -1 ? text : text.Substring(0, index);
        });
        _cache[path] = task;
    }
    return task;
}

public Task<string> FirstWordAsync() => FirstWordAsync("Default");

private static Task<T> Materialize<T>(Func<Task<T>> taskFactory) => taskFactory();

Upvotes: -1

Paulo Morgado
Paulo Morgado

Reputation: 14846

A task can be awaited more than once. Even after being completed.

A completed task adds very low overload to the operation.

So, you should cache the task.

And, if you want to make it concurrent, use a ConcurrentDictionary:

private ConcurrentDictionary<string,string> _cache = new ConcurrentDictionary<string,string>();
public Task<string> FirstWordAsync(string path)
    => _cache.GetOrAdd(path, () => Path.ReadAllTextAsync(path));

This way, any request for a path that is being read will receive a non-completed task and must wait for it to complete.

Upvotes: 0

Blindy
Blindy

Reputation: 67380

This transformation will also force me to do these kinds of things to manage overloads:

public async Task<string> FirstWord() => await FirstWord("Default");

No it doesn't, for one thing you can simply use default parameters, and even if not there's no purpose to awaiting a single function like that. The correct code, and the code the compiler generates, is this:

public Task<string> FirstWord() => FirstWord("Default");

Should this caching be limited by using a GetAwaiter().GetResult() ...?

Everything you read on the subject should be telling you never to use that pattern, because it will lead to dead-locks very easily. Use proper await/async.

Do not do async knowing that this approach will require a general transformation can be more expensive?

More expensive to whom? The whole idea of async programming is that you don't keep threads busy waiting for things they don't calculate, like I/O in your example. The thread that calls that function will be released to process the next request instead of sitting idle until the DMA operation on the file finishes.

IL add try / catch...

Just so we're clear, because even though it will absolutely not do it for the code you showed for this (with optimizations enabled, of course), try in general is essentially free in .Net. It's actually catching an exception that's expensive. Profile your code before jumping to conclusions!

Upvotes: 2

Related Questions