0

The Problem

I'm working on a web portal that loads a WebUser object when a user logs in via EF. WebUser has a non-trivial object graph and loading it via EF can take 2-3 seconds (optimizing that load time is a separate issue).

In order to improve perceived performance, I want to load the WebUser as soon as the user logs on to the system, on a separate thread. However, my current attempt runs synchronously for reasons that I do not understand.

The Code

static private ConcurrentDictionary<string, WebUser> userCache = 
        new ConcurrentDictionary<string, WebUser>();

static public void CacheProfile(string userName)
{
    if (!userCache.ContainsKey(userName)) 
    {
        logger.Debug("In CacheProfile() and there is no profile in cache");
        Task bg = GetProfileAsync(userName);
        logger.Debug("Done CacheProfile()");
    }
}

static public async Task<WebUser> GetProfileAsync(string userName)
{
    logger.Debug("GetProfileAsync for " + userName);

    await currentlyLoading.NotInSet(userName); // See NOTE 1 below

    if (userCache.ContainsKey(userName))
    {
        logger.Debug("GetProfileAsync from memory cache for " + userName);
        return userCache[userName];
    }
    else
    {
        currentlyLoading.Add(userName);

        logger.Debug("GetProfileAsync from DB for " + userName);

        using (MembershipContext ctx = new MembershipContext())
        {
            ctx.Configuration.LazyLoadingEnabled = false;
            ctx.Configuration.ProxyCreationEnabled = false;
            ctx.Configuration.AutoDetectChangesEnabled = false;

            var wu = GetProfileForUpdate_ExpensiveMethod(ctx, userName);
            userCache[userName] = wu;
            currentlyLoading.Remove(userName);

            return wu;
        }
    }

}

NOTE 1: currentlyLoading is a static instance of ConcurrentWaitUntil<T>. The intent is to cause a second request for a given user's profile to block if the first request is still loading from the database. Perhaps there is a better way to accomplish this? Code:

public class ConcurrentWaitUntil<T>
{
    private HashSet<T> set = new HashSet<T>();
    private Dictionary<T, TaskCompletionSource<bool>> completions = new Dictionary<T, TaskCompletionSource<bool>>();

    private object locker = new object();

    public async Task NotInSet(T item)
    {
        TaskCompletionSource<bool> completion;

        lock (locker)
        {
            if (!set.Contains(item)) return;

            completion = new TaskCompletionSource<bool>();
            completions.Add(item, completion);
        }

        await completion.Task;
    }

    public void Add(T item)
    {
        lock (locker)
        {
            set.Add(item);
        }
    }

    public void Remove(T item)
    {
        lock (locker)
        {
            set.Remove(item);

            TaskCompletionSource<bool> completion;
            bool found = completions.TryGetValue(item, out completion);

            if (found)
            {
                completions.Remove(item);

                completion.SetResult(true); // This will allow NotInSet() to complete
            }
        }
    }
}

The Question

Why does CacheProfile() seem to wait until GetProfileAsync() has completed?

SIDE NOTE: I know that the ConcurrentDictionary does not scale well and that I should use ASP.Net's cache.

Eric J.
  • 147,927
  • 63
  • 340
  • 553

3 Answers3

2

Why does CacheProfile() seem to wait until GetProfileAsync() has completed?

It sounds like your GetProfileAsync is first doing synchronous database calls and then performing some asynchronous operation.

Since you're using EF, you could fix this by upgrading to EF6 and using asynchronous queries.

Alternatively, Task.Run would make it work, but that is not recommended on the server side because it hurts your scalability.

On a side note, I prefer to structure in-memory asynchronous caches so that they cache tasks instead of results, so something like this:

static private ConcurrentDictionary<string, Task<WebUser>> userCache = new ConcurrentDictionary<string, Task<WebUser>>();

static public Task<WebUser> GetProfileAsync(string userName)
{
  return userCache.GetOrAdd(userName, _ =>
  {
    logger.Debug("In GetProfileAsync() and there is no profile in cache");
    return LoadProfileAsync(userName);
  });
}

static private async Task<WebUser> LoadProfileAsync(string userName)
{
  // Load it from DB using EF6 async queries.
  // Don't block other callers.

  logger.Debug("Loading from DB complete");
}

Then on the initial login, you can just call GetProfileAsync and ignore the result.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I have added the complete code of `GetProfileAsync()`. The first method call is an `await`. I realize there is a potential race condition between that line and the line that adds the user name to the list of profiles currently loading, but the second request is triggered by a physical click so is not actually happening in this case. I'm looking at your pattern for caching Tasks rather than results now. – Eric J. Oct 22 '13 at 15:47
  • On a side note, it never before occurred to me that `_` is a valid parameter name. – Eric J. Oct 22 '13 at 16:17
  • When caching the Task rather than the result, how would I update the in-memory cache? There is code that updates the in-memory representation and also separately writes changes to the DB. This is due to http://entityframework.codeplex.com/workitem/864 – Eric J. Oct 22 '13 at 16:21
  • An `await` does not guarantee asynchronicity. It only *allows* it. I.e., in your existing code, the first time a user profile is loaded, `NotInSet` returns a completed task (synchronously), so `await` does not return to its caller. I have an [`async` intro](http://blog.stephencleary.com/2012/02/async-and-await.html) on my blog if that's not clear. Also, by default `await` will capture the current context and resume the `async` method in that context; in ASP.NET, that's a request context, and you can't resume in the context if the request completes. Consider using `ConfigureAwait(false)`. – Stephen Cleary Oct 22 '13 at 16:28
  • Regarding updates, you'd have to decide on the exact semantics you want (i.e., how to handle an update notification when a read for that profile is already in progress). Sorry, I'm not an advanced EF user so I'm not clear on the details (that may be good as a separate EF-tagged question). When you know all the semantics, you can use [`AddOrUpdate`](http://msdn.microsoft.com/en-us/library/ee378675(v=vs.110).aspx) for the implementation. – Stephen Cleary Oct 22 '13 at 16:37
  • I follow a (personal) convention of `_` for unused parameters in lambda expressions, with `__` if I need a second. I've seen others use this convention as well, and there's at least one language out there (not C#) that actually has `_` as a special name that can be duplicated for any number of parameters in the same scope - but (obviously) those variables cannot actually be used. – Stephen Cleary Oct 22 '13 at 16:40
0

Your CacheProfile method needs to be async as well. Otherwise, it will not end until it gets result from GetProfileAsync

If you just want fire and forget use this:

static void ReadAndCache()
{
    // retrieve and place in cache
}

static public void GetProfileAsync(string userName)
{
    Task.Run(() => ReadAndCache());
}
evhen14
  • 1,839
  • 12
  • 16
  • I specifically do not want `CacheProfile` to be async, because that would also require the controller action to be async. I'm *not* looking for that behavior. I want the controller to return *and complete* after starting the `Task`, not create an async controller. – Eric J. Oct 22 '13 at 02:48
  • @EricJ. Do you intend to use data returned by Task? If not, just fire it and forget. If you want to get result from task, you will need to wait for it to complete. In the meantime, when you wait, you can run some other work – evhen14 Oct 22 '13 at 02:53
  • The data does not need to be used by the controller action. I am trying to "fire and forget" but find that the controller action actually blocks until the task that it starts completes. I have no idea why it is blocking. – Eric J. Oct 22 '13 at 02:56
  • @EricJ. Updated the answer a bit. What we do here, is just fire a new task – evhen14 Oct 22 '13 at 03:06
  • `GetProfileAsync()` is already marked with the `async` keyword (and it needs to be because the implementation uses `await`. I cannot easily redesign it to be non-async. – Eric J. Oct 22 '13 at 05:32
0

I think using await will get you desired behavior.Please look at the following

static public void CacheProfile(string userName)
{
    if (!userCache.ContainsKey(userName)) 
    {
        logger.Debug("In CacheProfile() and there is no profile in cache");
        Task bg = GetProfileAsync(userName); 
        logger.Debug("Done CacheProfile()");
    }
}

static public async Task<WebUser> GetProfileAsync(string userName)
{
    //load you data here.
    // will create a new thread and start task and return to the calling method.
    return await Task.Run(() => { 
                //your code which fetches data goes here
                //which returns a "WebUser" object
            });
    logger.Debug("Loading from DB complete");
}
Prerak K
  • 10,940
  • 7
  • 30
  • 37