Amir Popovich
Amir Popovich

Reputation: 29846

Adding a synchronous caching mechanism to an async method "transparently"

I have a method which does a long action using an async task
Now, I want to add a cache mechanism that will be transparent in the same method. Now, I could always fetch my cache result and wrap it with a Task so it will "work" but I want to prevent the context switch that I will get.

Here's an example of what I have:

var result = await LongTask();

private async Task<string> LongTask()
{
   return await DoSomethingLong();
}

And here's an example of what I want:

var result = await LongTask();

private async Task<string> LongTask()
{
   if(isInCache)
   { 
      return cachedValue(); // cache value is a const string you can do return "1" instead.
   }

   // else do the long thing and return my Task<string>
   return await DoSomethingLong();
}

Now I'm surprised to see that this compiled and worked
Something tells me that I'm not doing it correctly.

Here's another similar example that I've tested:

private async Task<string> DownloadString(bool sync)
{
    using (WebClient wc = new WebClient())
    {
        var task = wc.DownloadStringTaskAsync("http://www.nba.com");

        if(sync)
            return task.Result;

        return await task;
    }
}

And here's the code:

var res = DownloadString(true);
string str1 = await res;
var res2 = DownloadString(false);
string str2 = await res2;

From what I've read here task.Result executes the task synchronously and returns a string. Now I see the request via Fiddler and my program get's stuck on the return task.Result line even though I see a 200 OK and I wait a long time.

Bottom Line:

  1. Whats the best\correct way to use caching inside an async method(e.g. doing something synchronously in some cases without create a context switch overhead?
  2. Why does my second block of code with the DownloadString get stuck?

Upvotes: 5

Views: 2158

Answers (1)

i3arnon
i3arnon

Reputation: 116636

First of all, if after a call to an async method the returned task is already completed there would be no context switch, because none is needed. So this is completely acceptable:

private async Task<string> LongTask()
{
   if(isInCache)
   { 
      return cachedValue(); // cache value is a const string you can do return "1" instead.
   }

   // else do the long thing and return my Task<string>
   return await DoSomethingLong();
}

However, in the cases where the result is cached, the async mechanism is redundant. This overhead is mostly negligible but you can improve performance by dropping both the async and await and create a completed task using Task.FromResult:

private Task<string> LongTask()
{
   if(isInCache)
   { 
      return Task.FromResult(cachedValue()); 
   }

   // else do the long thing and return my Task<string>
   return DoSomethingLong();
}

...when you write “await someObject;” the compiler will generate code that checks whether the operation represented by someObject has already completed. If it has, execution continues synchronously over the await point. If it hasn’t, the generated code will hook up a continuation delegate to the awaited object such that when the represented operation completes, that continuation delegate will be invoked

From Async/Await FAQ


Task.Result doesn't execute the task synchronously, it waits synchronously. That means that the calling thread is blocked waiting for the task to complete. When you use that in an environment with a SynchronizationContext that may lead to a deadlock since the thread is blocked and can't handle the task's completion. You shouldn't use the Result property on a task that hasn't completed yet.

Upvotes: 6

Related Questions