管理ASP.NET Web窗体中长时间运行任务的策略

Sve*_*sen 4 c# asp.net .net-4.5

我们有一些逻辑来为我们的ASP.NET Web窗体应用程序计算每个用户的昂贵价值.目前它位于Page_Load每个页面上的标题用户控件中,如下所示:

//note that we are not awaiting this
Task.Factory.StartNew(() => CacheManager.GetExpensiveValue(UserId));
Run Code Online (Sandbox Code Playgroud)

然后在静态中CacheManager.GetExpensiveValue(int userID):

private static object locker = new object();

lock (locker)
{
    if (!AlreadyDone(userID))
    {
        var expensiveValue = ReallyExpensiveMethod(userID);
        //our static cache wrapper class that uses an ObjectCache object
        OurCache.Add(userID, expensiveValue);
    }
    else
    {
        return OurCache.Get(userID);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是有效的,但是当ReallyExpensiveMethod()花了很长时间(我也在努力提高其背后的逻辑性能)时,用户将lock在页面之间导航时阻止它.

我的问题是,我怎么能重组这个不导致阻塞?我已经考虑过使用ConcurrentDictionary,字典中的值是Task包装器,ReallyExpensiveMethod()而键是UserID以防止重复工作,但我不确定这是否真的让我到处都是.

我们目前在这个应用程序中没有使用任何异步逻辑,而且我确信这些功能不会引入需要添加Async="true"到应用程序中每个页面的更改,因为此标头逻辑位于每个页面中.

Ste*_*ary 6

我的问题是,我怎么能重组这个不导致阻塞?...而不是介绍[异步]

你在那里的岩石和坚硬的地方之间.任何请求都必须阻止或异步等待进程完成; 没有其他选择,除非您可以使用SignalR之类的东西将流程结果发送到客户端(但这可能需要进行重大的架构更改).

也就是说,你当然可以最大限度地减少其影响lock; 如果一个用户正在执行该过程,它目前阻止其他用户获取.

我假设这个计算是纯粹的(没有副作用),并且缓存是一个进程内的内存缓存.

在这种情况下,我会缓存任务而不是结果.虽然我对ASP.NET上的并行处理并不狂热,但我认为这样就行了.

我建议你使用缓存.ConcurrentDictionary有类似的逻辑,但没有简单的方法来刷新旧条目.

所以,像这样:

// In Page_Load
CacheManager.GetOrAdd(UserID);

Task<Results> CacheManager.GetOrAdd(int userId)
{
  lock (locker)
  {
    if (!OurCache.Contains(userId))
    {
      var task = Task.Run(() => ReallyExpensiveMethod(userId));
      OurCache.Add(userId, task);
      return task;
    }
    else
      return OurCache.Get(userId);
  }
}

// Usage:
Results results = CacheManager.GetOrAdd(UserID).Result;
Run Code Online (Sandbox Code Playgroud)

我并不喜欢阻塞(调用Task<T>.Result最后一行),但由于你不想做异步请求,你会遇到这种黑客攻击.

此代码最大限度地减少了锁定的时间.它不是在处理期间锁定它,而是仅锁定足够长的时间以在另一个线程上启动处理并更新缓存.