问题
我正在开发一个Web门户,当用户通过EF登录时,它会加载WebUser
个对象。 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:currentlyLoading
是ConcurrentWaitUntil<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()
完成?
SIDE注意:我知道ConcurrentDictionary
不能很好地扩展,我应该使用ASP.Net的缓存。
答案 0 :(得分:2)
为什么CacheProfile()似乎要等到GetProfileAsync()完成?
听起来你GetProfileAsync
首先进行同步数据库调用,而然后执行一些异步操作。
由于您使用的是EF,因此可以通过升级到EF6并使用asynchronous queries来解决此问题。
或者,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
并忽略结果。
答案 1 :(得分:0)
您的CacheProfile方法也需要异步。否则,它将不会结束,直到它从GetProfileAsync
获得结果如果你只是想要开火而忘记使用它:
static void ReadAndCache()
{
// retrieve and place in cache
}
static public void GetProfileAsync(string userName)
{
Task.Run(() => ReadAndCache());
}
答案 2 :(得分: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");
}