在同步运行的Controller操作中启动任务

时间:2013-10-22 01:39:04

标签: asp.net-mvc multithreading

问题

我正在开发一个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: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()完成?

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

3 个答案:

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