双线程访问同一List触发System.ArgumentOutOfRangeException该如何处理?
异常根本原因
你碰到的问题本质由两个原因导致:
List<T>属于非线程安全类型,跨线程同时读写没有同步保护时,除了索引越界,还可能出现集合状态损坏、脏数据读取等问题- 没有做执行时序同步,T2执行读操作的时机早于T1完成所有元素写入的时机,此时集合为空,访问索引0自然抛出越界异常
不同场景的解决方案
场景1:T1必须完成所有元素添加后,T2才允许访问集合
这是最贴合你问题描述的场景,推荐用ManualResetEventSlim做轻量级时序同步:
private readonly List<item> myCompletedItems = new List<item>(); // 初始化信号为未触发状态 private readonly ManualResetEventSlim _addCompletedSignal = new ManualResetEventSlim(false);
T1所有元素添加完成后触发信号:
// T1执行的批量添加逻辑 public void AddAllItems(List<item> itemsToAdd) { foreach (var item in itemsToAdd) { myCompletedItems.Add(item); } // 所有元素添加完成,标记信号为已触发 _addCompletedSignal.Set(); }
T2调用的读方法先等待信号触发再访问集合:
public int GetStartItemId() { // 等待T1完成添加,也可以传入超时参数避免无限阻塞 _addCompletedSignal.Wait(); return myCompletedItems[0].id; }
如果提前知道T1要添加的元素总数,也可以用CountdownEvent,每添加1个元素调用Signal()减计数,T2等待计数归0后再访问即可。
场景2:不需要等所有元素加完,T2只要保证读时集合非空、操作线程安全
如果业务允许T1加一个T2就读一个,不需要等全部添加完成,直接用.NET内置的线程安全集合BlockingCollection<T>即可,天然支持阻塞读直到有元素可用:
// 底层默认用ConcurrentQueue<T>存储,也可以指定其他线程安全集合 private readonly BlockingCollection<item> myCompletedItems = new BlockingCollection<item>();
添加逻辑基本无需修改:
public void ItemCreated(item theitem) { myCompletedItems.Add(theitem); // 所有元素添加完成后调用该方法,后续读操作不会再阻塞等待新元素 // myCompletedItems.CompleteAdding(); }
读方法自动阻塞直到有元素可取:
public int GetStartItemId() { var firstItem = myCompletedItems.Take(); return firstItem.id; }
该方案不需要手动写同步逻辑,出错概率低,非常适合生产者-消费者场景。
场景3:不想修改原有集合定义,需要兼容旧逻辑
如果不想改动现有集合类型,用lock加完成标志位也可以实现需求:
private readonly List<item> myCompletedItems = new List<item>(); private readonly object _lockObj = new object(); private bool _isAddCompleted = false;
添加完成后修改标志位唤醒读线程:
public void AddAllItems(List<item> itemsToAdd) { lock (_lockObj) { foreach (var item in itemsToAdd) { myCompletedItems.Add(item); } _isAddCompleted = true; Monitor.PulseAll(_lockObj); } }
读方法循环等待添加完成:
public int GetStartItemId() { lock (_lockObj) { // 循环等待避免虚假唤醒 while (!_isAddCompleted) { Monitor.Wait(_lockObj); } return myCompletedItems[0].id; } }
注意事项
- 任何场景下都不要无同步跨线程访问非线程安全集合,除了索引越界,还可能触发更难排查的隐式集合损坏问题
- 阻塞等待方法建议传入超时参数,避免业务异常时线程无限挂起
内容的提问来源于stack exchange,提问作者Mr.Curious
相关产品推荐
相关产品推荐

