我们有一些逻辑来为我们的 ASP.NET Web 窗体应用程序计算每个用户的昂贵值(value)。目前它位于每个页面上的 Page_Load
header 用户控件中,如下所示:
//note that we are not awaiting this
Task.Factory.StartNew(() => CacheManager.GetExpensiveValue(UserId));
然后在静态 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);
}
}
这行得通,但是当 ReallyExpensiveMethod()
花费了很长时间(我也在努力提高其背后逻辑的性能)时,用户将阻塞那个 lock
在页面之间导航时。
我的问题是,我如何重组它才不会导致阻塞?我考虑过使用 ConcurrentDictionary,字典中的值是 ReallyExpensiveMethod()
的任务包装器,键是 UserID 以防止重复工作,但我不确定这是否真的能让我满意任何地方。
我们目前没有在这个应用程序中使用任何异步逻辑,我相信当权者宁愿不引入需要添加 Async="true"
到每个应用程序中的单个页面,因为此 header 逻辑在每个页面中。
最佳答案
My question is, how could I restructure this to not cause blocking? ... rather not introduce [asynchrony]
你进退两难。任何请求都必须阻塞或异步等待进程完成;没有其他选择,除非您可以使用 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;
我对阻塞并不狂热(在最后一行调用 Task<T>.Result
),但由于您不想执行异步请求,所以您遇到了这种 hack。
此代码最大限度地减少了持有锁的时间。它不是在处理期间锁定它,而是锁定足够长的时间以在另一个线程上开始处理并更新缓存。
关于c# - 在 ASP.NET Web 窗体中管理长时间运行的任务的策略,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23408710/