在我正在处理的程序(ASP.NET 应用程序)中,我有一个静态只读的内存中查找集合(在 ASP.NET 应用程序的所有线程之间共享)。
我知道这不是正统,但这是为了拥有远程且缓慢的数据库的内存版本,以提高某些查找操作的性能。
然后需要执行查找的 ASP.NET 页面访问该集合,因此它可以被许多线程访问,但这不是问题,因为可以保证这些线程只读取该集合。
有时我需要刷新集合内容,为了在后台线程(带锁)中执行此操作,我会处理相同类型的静态查找集合的临时变量。
当我完成所有工作后,我只需将临时变量分配给静态查找集合,如果同时“交换”完成,有人访问了旧数据,那不是问题(只要迟早会接受读取旧数据)数据将会更新)。
我认为不应该有任何问题,但我有点害怕这种情况的竞争条件。另一个让我有点害怕的方面是内存泄漏。
我开始考虑 Interlock.Exchange,但不确定是否适合我的情况。
这是我用来处理内存中集合的基类,它表示缓慢且远程数据库的查找结构:
//In memory indexed collection to query slow table by key when table does not contain too much data and is somehow slow (eh remote system)
public class InMemoryStore<T,K>
where T : new()
where K : struct
{
protected object _instanceSync = new object();
protected DateTime _lastRebuild = ConstantsEntry.MIN_DATE;
protected TimeSpan _timeout = new TimeSpan(0, 15, 0);
protected int _cutoff = 1000;
protected ReadOnlyDictionary<K, T> _preparingData = null;
protected ReadOnlyDictionary<K, T> _readyData = null;
protected volatile bool _isSyncRunning = false;
protected DateTime _syncronizationStart = ConstantsEntry.MIN_DATE;
protected InMemoryStore(TimeSpan timeout, int cutoff)
{
_timeout = timeout;
_cutoff = cutoff;
}
// Best effort query against the store using only key as search means
public List<T> Query ( List<K> query)
{
List<T> result = new List<T>();
var dictionaryRef = _readyData;
if (dictionaryRef == null)
return result;
foreach (var key in query)
if (dictionaryRef.TryGetValue(key, out T value))
result.Add(value);
return result;
}
// Standard logic to rebuild internal index, provide some extension point to customize memory constraints and table access
public void Rebuild()
{
try
{
lock (_instanceSync)
{
if (_isSyncRunning && (DateTime.UtcNow - _syncronizationStart) < new TimeSpan(0,30,0) )
return;
if (this.MemoryLimitExceeded(cutoff: _cutoff))
{
_readyData = null;
_preparingData = null;
}
_preparingData = null;
_isSyncRunning = true;
_syncronizationStart = DateTime.UtcNow;
Task.Run(() =>
{
try
{
this.InternalRebuildLogic();
if (_preparingData != null)
{
_readyData = _preparingData
//or better --> Interlocked.Exchange<ReadOnlyDictionary<K, T>>(ref _readyData, _preparingData);
_preparingData = null;
_lastRebuild = DateTime.UtcNow;
}
}
catch (Exception threadErr) { }
finally
{
_isSyncRunning = false;
}
});
}
}
catch (Exception err) { }
}
//Extension point to execute custom index rebuild
protected virtual void InternalRebuildLogic() {}
// Check there is not too much item in the collection
protected virtual bool MemoryLimitExceeded (int cutoff) {}
}
这是一个具体的实现:
public class ArticleInMemoryStore : InMemoryStore<tbl_ana_Article, int>
{
protected ArticleInMemoryStore(TimeSpan timeout, int cutoff) : base(timeout, cutoff){}
protected override bool MemoryLimitExceeded(int cutoff)
{
using ( var context = ServiceLocator.ConnectionProvider.Instance<ArticleDataContext>())
{
int entries = context.tbl_ana_Articles.Count();
return entries > cutoff;
}
}
protected override void InternalRebuildLogic()
{
using (var context = ServiceLocator.ConnectionProvider.Instance<ArticleDataContext>())
{
var dictionary = context.tbl_ana_Articles.ToList().ToDictionary(item => item.ID, item => item);
_preparingData = new ReadOnlyDictionary<int,tbl_ana_Article>(dictionary);
}
}
}
我想就与内存泄漏和竞争条件相关的可能问题获得反馈,在这种情况下我可能忽略了这些问题。
例如,我问自己使用
Interlocked.Exchange
是否比我那样仅仅变量赋值更好。
我在这里看到一个问题:
protected ReadOnlyDictionary<K, T> _readyData = null;
_readyData
字段不是volatile
,而是在Query
方法内部访问,无需同步或显式内存屏障:
var dictionaryRef = _readyData;
没有围栏的问题是,.NET Jitter 允许以允许运行
Query
方法的线程查看部分初始化的 ReadOnlyDictionary<K, T>
对象的方式重新排序指令。发生这种情况的概率很小,并且根据程序运行的硬件,它可能为零。如果你不喜欢赌博,我的建议是将该字段声明为volatile
。
您可以找到更详细地解释此问题中与缺乏内存屏障相关的危险的答案:.NET 中双重检查锁定中需要 volatile 修饰符。