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

仅更新字典值场景下Dictionary/ConcurrentDictionary/FASTER KV选型

场景说明

当前需要处理股票实时行情流传输业务,业务规则如下:

  • 每只股票以无符号整型作为键,对应存储的值为struct类型Tick(非class类型)
  • 初始采用Dictionary<uint, Tick>作为存储结构,该字典在初始化阶段一次性写入全部共5000个键,初始化完成后不再执行新增、删除键操作
  • 仅由WebSocket回调随机更新已有键对应的Tick值,回调触发频率约为每毫秒1次
  • 另有两个独立后台线程遍历该字典读取最新值用于行情分析:其中一个线程每20毫秒完成一轮遍历后休眠,另一个线程持续无休眠遍历

已知普通Dictionary不是线程安全类型,ConcurrentDictionary存在一定性能开销,核心疑问为该场景下是否更适合选用Microsoft FASTER KV,同时附带两个具体问题:

  1. 若不对字典执行新增/删除键操作,仅由单线程更新已有键的值、其他线程执行读取/遍历操作,普通Dictionary是否具备线程安全性?
  2. 下方代码块中标记的Point A和Point B位置获取到的值是否一致?
/// <summary>
/// this struct from 3rdpary library
/// </summary>
public struct Tick
{
    public DateTime DateTime { get; set; }
    public uint InstrumentToken { get; set; }
    public decimal LastPrice { get; set; }
}

class TickDataProcessor
{
    private Dictionary<UInt32, Tick> AllStocksTickData { get; set; }
    private Dictionary<UInt32, Tick> AllPreviousStocksTickData { get; set; }


    private void Initialize()
    {
        AllPreviousStocksTickData = new Dictionary<uint, Tick>(capacity: 5000);
        AllPreviousStocksTickData = new Dictionary<uint, Tick>(capacity: 5000);

        // initialize dict with all empty values, after this no more additions
        UInt32 key = 1000001;
        for (int i = 0; i < 5000; i++)
        {
            AllStocksTickData[key] = new Tick();
            AllPreviousStocksTickData[key] = new Tick();
            key++;
        }
    }

    /// <summary>
    ///real time streaming of stock data from stock broker
    /// this method is call back of websocket, websocket callback comes every millisecond for random key
    /// it will update the value of that key.
    /// </summary>
    /// <param name="tickData"></param>
    public void OnNewTickData(Tick tickData)
    {
        AllStocksTickData[tickData.InstrumentToken] = tickData;
    }

    public void Start()
    {
        var ts = new ThreadStart(OneBackgroundMethodStockPriceAnalyzer);
        var backgroundThread = new Thread(ts);
        backgroundThread.Start();

        var ts2 = new ThreadStart(TwoBackgroundMethodStockPriceAnalyzer);
        var backgroundThread2 = new Thread(ts2);
        backgroundThread2.Start();
    }

    private void TwoBackgroundMethodStockPriceAnalyzer()
    {
        while (true)
        {
            foreach (var entry in AllStocksTickData)
            {
                // second thread
            }
        }
    }

  private  void OneBackgroundMethodStockPriceAnalyzer()
    {
        while (true)
        {
            foreach (var entry in AllStocksTickData)
            {
                var key = entry.Key;
                var tickData = entry.Value; // Point A
                /*
                 * process tickdata
                 *
                 */
                if (AllPreviousStocksTickData[key].LastPrice >= tickData.LastPrice)
                {
                    // calculate % of change
                    // other calculations...
                }

                AllPreviousStocksTickData[key] = entry.Value; // Point B
            }
            Thread.Sleep(20);
        }
    }
}

class Program
{
    public static void Main(string[] args)
    {
        var tickDataProcessor = new TickDataProcessor();
        tickDataProcessor.Start();
        Console.ReadLine();
    }
}
问题解答

疑问1:固定键的单写多读场景下普通Dictionary的线程安全性

不具备线程安全性。

  • 哪怕没有新增/删除键操作,Dictionary<TKey, TValue>的已有键更新和遍历/读取操作没有做任何内存屏障、读写同步保证:
    1. 值类型更新时如果大小超过CPU字长(本场景中的Tick包含DateTime、uint、decimal,大小远大于8字节),读线程可能读到撕裂值——也就是写线程只更新了一半字段时,读线程就把半新半旧的值读出来,拿到完全无效的行情数据
    2. 即使初始化时给定了足够容量、不会触发扩容,JIT和CPU仍可能对字典条目的读写指令做重排,读线程遍历的时候可能读到条目状态不一致的情况,极端场景下甚至会触发遍历异常、死循环
  • .NET官方文档明确标注Dictionary所有实例成员都不保证线程安全,并发读写场景下出现任何未定义行为都属于预期内结果,不存在“测试没出问题就可以用”的侥幸空间。

疑问2:Point A和Point B的值一致性

完全不保证一致。

  • Point A的tickData是遍历KeyValuePair时拿到的Tick结构体副本,赋值完成后这个值就存储在当前线程的栈上,和原字典里存储的值已经没有关联
  • 从Point A到Point B的执行间隙,WebSocket回调线程完全可能把同一个键对应的Tick更新成新值,此时entry.Value会读取到更新后的新副本,和之前栈上存储的tickData不是同一个值
  • 额外说明:示例代码本身存在初始化bug,Initialize方法中两次实例化赋值给AllPreviousStocksTickData,AllStocksTickData从未被实例化,运行时会直接抛出空引用异常;且AllPreviousStocksTickData被后台线程遍历的同时执行写入操作,本身也存在严重的线程安全问题。
高性能场景下的存储方案选型

首先明确结论:该场景完全不适合选用Microsoft FASTER KV,属于典型的过度选型。
FASTER KV的核心优势是处理远超内存容量的超大规模数据、支持持久化、高吞吐的混合读写工作负载,本场景总共只有5000个固定键,全量数据加起来仅几MB,FASTER的日志架构、异步持久化、设备IO层等核心能力完全用不上,反而会引入额外的序列化、异步提交开销,性能远低于纯内存级方案。

也不建议直接使用默认配置的ConcurrentDictionary:默认的ConcurrentDictionary更新操作会采取细粒度锁,遍历的时候会走版本校验,对固定键、单写多读的场景来说仍有不必要的开销。

按性能从高到低排序,适合本场景的方案如下:

  1. 最优方案:数组存储+Volatile读写无锁同步
    由于业务中的股票键是连续从1000001开始的连续值,可直接将键做偏移映射到数组下标,数组内存连续,遍历性能比字典高一个量级,访问开销几乎为0。更新值时用Volatile.Write写入整个Tick结构体,读取时用Volatile.Read拿到完整的值副本,全程无锁,不会出现撕裂值问题,性能比ConcurrentDictionary高3~5倍,完全能支撑当前的更新、遍历频率。
  2. 次优方案:普通Dictionary+ReaderWriterLockSlim
    如果不想做数组映射改造,可给普通Dictionary搭配读写锁:写操作拿写锁,读/遍历操作拿读锁。5000个键的遍历加读锁的开销几乎可以忽略,写锁的持有时间仅几百纳秒,完全能支撑每毫秒1次的更新频率,性能优于FASTER和默认配置的ConcurrentDictionary。
  3. 兜底方案:预初始化容量的ConcurrentDictionary
    如果一定要用现成的并发集合,初始化时将ConcurrentDictionary容量设为5000避免扩容,更新值时使用TryUpdate方法而非索引器直接赋值,可将开销压到最低,但性能仍弱于前两种方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 02:57:09