Nathan Friend Nathan Friend - 2 months ago 5
C# Question

Use a Task to avoid multiple calls to expensive operation and to cache its result

I have an async method that fetches some data from a database. This operation is fairly expensive, and takes a long time to complete. As a result, I'd like to cache the method's return value. However, it's possible that the async method will be called multiple times before its initial execution has a chance to return and save its result to the cache, resulting in multiple calls to this expensive operation.

To avoid this, I'm currently reusing a

Task
, like so:

public class DataAccess
{
private Task<MyData> _getDataTask;

public async Task<MyData> GetDataAsync()
{
if (_getDataTask == null)
{
_getDataTask = Task.Run(() => synchronousDataAccessMethod());
}

return await _getDataTask;
}
}


My thought is that the initial call to
GetDataAsync
will kick off the
synchronousDataAccessMethod
method in a
Task
, and any subsequent calls to this method before the
Task
has completed will simply await the already running
Task
, automatically avoiding calling
synchronousDataAccessMethod
more than once. Calls made to
GetDataAsync
after the private
Task
has completed will cause the
Task
to be awaited, which will immediately return the data from its initial execution.

This seems to be working, but I'm having some strange performance issues that I suspect may be tied to this approach. Specifically, awaiting
_getDataTask
after it has completed takes several seconds (and locks the UI thread), even though the
synchronousDataAccessMethod
call is not called.

Am I misusing async/await? Is there a hidden gotcha that I'm not seeing? Is there a better way to accomplish the desired behavior?

EDIT

Here's how I call this method:

var result = (await myDataAccessObject.GetDataAsync()).ToList();


Maybe it has something to do with the fact that the result is not immediately enumerated?

Answer

Use the lock function to prevent multiple calls to the database query section. Lock will make it thread safe so that once it has been cached all the other calls will use it instead of running to the database for fulfillment.

lock(StaticObject) // Create a static object so there is only one value defined for this routine
{
    if(_getDataTask == null)
    {
         // Get data code here
    }
    return _getDataTask
}
Comments