You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带同步机制的查找集合:线程安全与内存泄漏问题咨询

问题场景

我正在开发一个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 修饰,原因如下:

  1. 保证可见性:Interlocked.Exchange 会强制刷新CPU缓存,确保所有线程立即看到 _readyData 的最新引用值,解决了直接赋值时的可见性问题。
  2. 原子性保障:虽然引用赋值本身是原子的,但 Interlocked.Exchange 可以确保赋值操作的原子性和内存屏障,避免极端情况下的指令重排问题。
  3. 代码可读性:明确表达这是一个多线程环境下的共享变量更新操作,让后续维护者清楚这里的线程安全逻辑。

修改示例:

// 替换原来的直接赋值
Interlocked.Exchange(ref _readyData, _preparingData);

同时,建议给 _preparingData 加上 volatile 修饰,确保后台线程能正确读取到 InternalRebuildLogic 设置的最新值:

protected volatile ReadOnlyDictionary<K, T> _preparingData = null;

内容的提问来源于stack exchange,提问作者Skary

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 03:53:10