带同步机制的查找集合:线程安全与内存泄漏问题咨询
我正在开发一个ASP.NET应用,其中包含一个被所有线程共享的static readonly内存查找集合,用来缓存远程慢数据库的数据以提升查询性能。该集合仅被多线程读取,本身无问题;有时需要刷新集合内容,我会在后台线程中借助锁操作临时变量,完成后将临时变量赋值给静态集合,刷新期间线程读取旧数据是可接受的。
我担心存在竞态条件及内存泄漏问题,同时不确定是否应使用Interlocked.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); } } }
1. _readyData 赋值的可见性问题
_readyData 未被标记为 volatile,直接赋值时,读取线程可能因为CPU缓存优化,无法立即看到最新的引用值,导致持续读取旧数据。虽然引用类型的赋值操作本身是原子的,但可见性无法保证,这会导致缓存刷新后,部分线程仍长时间使用旧集合。
2. _preparingData 的线程安全问题
_preparingData 既没有 volatile 修饰,也没有锁保护:
InternalRebuildLogic在后台线程中设置_preparingData,随后后台线程判断_preparingData != null再赋值给_readyData。由于没有可见性保证,后台线程可能读取到_preparingData的旧值(null),导致刷新逻辑白跑,_readyData无法更新。- 如果多个
Rebuild调用进入锁(超时后可能再次进入),_preparingData被反复置空,可能覆盖正在准备的新数据。
3. _lastRebuild 的读写不一致
_lastRebuild 在后台线程中直接赋值,没有任何同步机制。如果后续有逻辑依赖这个时间戳(比如判断是否需要触发刷新),读取线程可能获取到旧的时间值,导致逻辑错误。
4. _isSyncRunning 的边界情况
_isSyncRunning 是 volatile 的,lock内的判断逻辑基本安全,但后台线程在finally中设置 _isSyncRunning = false 时,没有锁保护。不过由于 volatile 保证了可见性,这个操作本身不会导致竞态,极端情况下也不会出现逻辑错误。
1. 集合对象的回收问题
由于 ReadOnlyDictionary 是不可变的,当 _readyData 被替换为新集合后,旧集合的引用只会被读取线程临时持有(Query方法中 dictionaryRef 是局部变量),线程完成查询后,局部变量被释放,旧集合会被GC正常回收,不会出现内存泄漏。
2. 数据库上下文的资源泄漏
ArticleInMemoryStore 中所有数据库上下文都用 using 包裹,会自动释放资源,不会导致连接泄漏或内存泄漏。
3. 异常吞掉的潜在风险
Rebuild 和后台线程中都吞掉了所有异常,虽然不会直接导致内存泄漏,但异常信息丢失会增加调试难度,建议至少记录日志。
4. ServiceLocator的潜在问题
如果 ServiceLocator.ConnectionProvider.Instance<ArticleDataContext>() 存在对象生命周期管理问题(比如返回的上下文没有正确实现Dispose),可能导致内存泄漏,但从代码看使用了 using,只要上下文本身实现正确,就不会有问题。
Interlocked.Exchange 替代直接赋值 建议使用 Interlocked.Exchange,或者给 _readyData 加上 volatile 修饰,原因如下:
- 保证可见性:
Interlocked.Exchange会强制刷新CPU缓存,确保所有线程立即看到_readyData的最新引用值,解决了直接赋值时的可见性问题。 - 原子性保障:虽然引用赋值本身是原子的,但
Interlocked.Exchange可以确保赋值操作的原子性和内存屏障,避免极端情况下的指令重排问题。 - 代码可读性:明确表达这是一个多线程环境下的共享变量更新操作,让后续维护者清楚这里的线程安全逻辑。
修改示例:
// 替换原来的直接赋值 Interlocked.Exchange(ref _readyData, _preparingData);
同时,建议给 _preparingData 加上 volatile 修饰,确保后台线程能正确读取到 InternalRebuildLogic 设置的最新值:
protected volatile ReadOnlyDictionary<K, T> _preparingData = null;
内容的提问来源于stack exchange,提问作者Skary

