0

问题

我正在开发一个WebUser在用户通过 EF 登录时加载对象的 Web 门户。 WebUser有一个重要的对象图,通过 EF 加载它可能需要 2-3 秒(优化加载时间是一个单独的问题)。

为了提高感知性能,我想WebUser在用户登录系统后立即在单独的线程上加载。但是,由于我不明白的原因,我当前的尝试同步运行。

编码

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;
        }
    }

}

注 1:currentlyLoadingConcurrentWaitUntil<T>. 目的是如果第一个请求仍在从数据库加载,则阻止对给定用户配置文件的第二个请求。也许有更好的方法来实现这一点?代码:

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
            }
        }
    }
}

问题

为什么CacheProfile()似乎要等到GetProfileAsync()完成?

旁注:我知道ConcurrentDictionary它不能很好地扩展,我应该使用 ASP.Net 的缓存。

4

3 回答 3

2

为什么 CacheProfile() 似乎要等到 GetProfileAsync() 完成?

听起来您GetProfileAsync首先进行同步数据库调用,然后执行一些异步操作。

由于您使用的是 EF,因此您可以通过升级到 EF6 并使用异步查询来解决此问题。

或者,Task.Run可以让它工作,但不建议在服务器端这样做,因为它会损害您的可伸缩性。

附带说明一下,我更喜欢构建内存中的异步缓存,以便它们缓存任务而不是结果,所以像这样:

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");
}

然后在初始登录时,您可以调用GetProfileAsync并忽略结果。

于 2013-10-22T12:31:55.397 回答
0

您的 CacheProfile 方法也需要是异步的。否则,直到从 GetProfileAsync 获得结果才结束

如果你只想着火而忘记使用这个:

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

static public void GetProfileAsync(string userName)
{
    Task.Run(() => ReadAndCache());
}
于 2013-10-22T01:42:06.843 回答
0

我认为使用await会得到你想要的行为。请看下面

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");
}
于 2013-10-22T09:09:44.203 回答