C#中访问持续增长List报错的解决方案咨询
解决高频WebSocket数据写入与多线程读取的并发问题
首先,咱们得先搞懂你遇到的这个「Destination array was not long enough」异常到底是怎么来的:List<T>内部是用数组存储元素的,当你调用Add导致元素数量超过当前数组容量时,它会自动扩容——创建一个更大的新数组,把旧数组的元素复制过去。如果这个扩容过程刚好和另一个线程的ToList()操作撞在一起,读取线程拿到的是正在被替换的旧数组信息,就会抛出这个数组长度不匹配的错误。
你之前只在读取侧加了锁,但WebSocket的Add操作完全没加同步,这就导致写入和读取的并发冲突,锁等于白加了。接下来给你几个可行的解决方案,从简单修复到性能优化逐步来:
1. 给所有List操作加上同一把锁
最直接的修复方式是确保写入和读取操作都用同一个锁对象来同步,而不是锁liveQuotesList本身(虽然这里是static List不会被重新赋值,但用单独的锁对象是更规范的做法)。
修正WebSocket端的写入代码:
// 定义全局锁对象,要保证WebSocket和读取线程都能访问到 private static readonly object _listSyncLock = new object(); public static List<LiveMarketDataObject> liveQuotesList = new List<LiveMarketDataObject>(); // 接收消息后添加元素时: lock (_listSyncLock) { liveQuotesList.Add(_liveQuotes); }
修正读取侧的代码:
// 复用同一个锁对象 private static readonly object _lock = socketWithEvaluation._listSyncLock; private static List<LiveMarketDataObject> liveQuotesList = socketWithEvaluation.liveQuotesList; // 查询时: lock (_lock) { var newQuoteList = liveQuotesList .Where(x => x.sym == symbolName && x.t >= startunixTime && x.t <= endunixTime) .ToList(); }
这样所有对liveQuotesList的操作都在锁的保护下,扩容和读取就不会冲突了。
2. 用ReaderWriterLockSlim提升并发性能
如果你的读取操作比写入更频繁,用普通的lock会让所有读线程互相阻塞,影响性能。这时候可以用ReaderWriterLockSlim——它允许多个读线程同时访问,只有写线程会独占锁,能大幅提升并发效率。
WebSocket端写入代码:
private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); public static List<LiveMarketDataObject> liveQuotesList = new List<LiveMarketDataObject>(); // 添加元素时: _rwLock.EnterWriteLock(); try { liveQuotesList.Add(_liveQuotes); } finally { // 必须在finally里释放锁,避免异常导致锁泄漏 _rwLock.ExitWriteLock(); }
读取侧代码:
private static ReaderWriterLockSlim _rwLock = socketWithEvaluation._rwLock; private static List<LiveMarketDataObject> liveQuotesList = socketWithEvaluation.liveQuotesList; // 查询时: _rwLock.EnterReadLock(); try { var newQuoteList = liveQuotesList .Where(x => x.sym == symbolName && x.t >= startunixTime && x.t <= endunixTime) .ToList(); } finally { _rwLock.ExitReadLock(); }
3. 优化数据结构,解决内存和查询效率问题
每分钟7万条数据,List会无限膨胀,不仅内存占用越来越高,每次查询遍历全量数据也会越来越慢。建议按时间分片存储,比如按小时划分数据块,同时自动清理旧数据:
WebSocket端分片写入代码:
// 用字典存储不同时间段的数据集,key是小时级别的时间戳 private static readonly Dictionary<long, List<LiveMarketDataObject>> _timePartitionedData = new Dictionary<long, List<LiveMarketDataObject>>(); private static readonly object _partitionLock = new object(); // 计算当前数据所属的小时分片(UTC时间,避免时区问题) long currentHour = (DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 3600) * 3600; lock (_partitionLock) { // 如果当前分片不存在,创建新的 if (!_timePartitionedData.ContainsKey(currentHour)) { _timePartitionedData[currentHour] = new List<LiveMarketDataObject>(); // 自动清理24小时前的旧分片,防止内存溢出 var expiredKeys = _timePartitionedData.Keys.Where(k => k < currentHour - 24 * 3600).ToList(); foreach (var key in expiredKeys) { _timePartitionedData.Remove(key); } } _timePartitionedData[currentHour].Add(_liveQuotes); }
读取侧分片查询代码:
var targetQuotes = new List<LiveMarketDataObject>(); // 计算查询时间范围覆盖的所有小时分片 long startHour = (startunixTime / 3600) * 3600; long endHour = (endunixTime / 3600) * 3600; lock (_partitionLock) { for (long hour = startHour; hour <= endHour; hour += 3600) { if (_timePartitionedData.TryGetValue(hour, out var chunk)) { // 只在当前分片里过滤数据,减少遍历量 targetQuotes.AddRange(chunk.Where(x => x.sym == symbolName && x.t >= startunixTime && x.t <= endunixTime)); } } }
这样既解决了并发问题,又优化了内存占用和查询效率,是高频数据场景下更合理的方案。
内容的提问来源于stack exchange,提问作者Ankit
相关产品推荐
相关产品推荐

