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

C#使用Task.WhenAll并发AddRange操作List报数组长度不足异常

问题背景

我在代码中使用Task.WhenAll发起多个携带不同参数的异步请求,按药房ID筛选对应患者ID,拉取不同药房(pharmacies)的患者(patients)数据,计划将所有返回结果汇总到同一个列表后,再映射为其他业务模型。
接口请求默认开启分页,页大小通常设置为50。代码大部分场景运行正常,但偶发抛出如下异常:
Destination array was not long enough. Check the destination index, length, and the array's lower bounds. (Parameter 'destinationArray')
约束要求:必须保留多请求异步并发执行的能力,不能改为串行调用。

原实现代码
var patients = new List<PatientDTO>();
await Task.WhenAll(
    pharmacyIds.Select(async pharmacyId =>
        patients.AddRange(await _patientService.GetPatientsByClientIds(pharmacyId, recipes
                .Where(x => x.PharmacyId == pharmacyId)
                .Select(x => x.PatientId)))));
异常堆栈
at System.Array.Copy(Array sourceArray, Int32 sourceIndex, Array destinationArray, Int32 destinationIndex, Int32 length, Boolean reliable)
   at System.Collections.Generic.List`1.Insert(Int32 index, T item)
   at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)
   at Coach.Domain.Queries.GetRecipeLogsQueryHandler.<>c__DisplayClass4_0.<<Handle>b__14>d.MoveNext()
问题根因

List<T>不是线程安全的集合,原生不支持多线程/多任务并发写入。
原代码中多个异步任务并发执行,请求完成后会同时调用共享patients列表的AddRange方法,存在明确的竞态条件:

  • 多个任务同时判定列表剩余容量充足,同时向同一内存位置写入数据
  • 某个任务触发列表扩容、替换内部存储数组时,其他任务仍向旧数组执行元素拷贝
  • 多个任务同时修改列表内部的写入位置索引,导致索引计算偏差
    你看到的数组长度不足错误就是这类竞态的典型表现。由于网络请求返回时间存在随机波动,多数场景下任务不会刚好同时执行写入操作,因此异常表现为偶发,和分页大小配置没有直接关联。
修复方案

不需要加锁,也不需要退化为串行调用,最优实现是避免并发任务直接写入共享集合:让每个异步任务仅返回自身的查询结果,等所有任务全部执行完成后,再在单一线程上下文中统一汇总所有结果,既完全保留并发能力,又没有锁开销:

// 并发发起所有查询任务,每个任务独立返回自己的结果集,不操作共享变量
var queryTasks = pharmacyIds.Select(async pharmacyId =>
    await _patientService.GetPatientsByClientIds(pharmacyId, recipes
        .Where(x => x.PharmacyId == pharmacyId)
        .Select(x => x.PatientId))
).ToArray();

// 等待所有查询请求完成
var queryResults = await Task.WhenAll(queryTasks);

// 单线程汇总所有结果,不存在并发写入问题
var patients = queryResults.SelectMany(result => result).ToList();

该方案的优势:

  • 所有查询请求完全并发执行,和原逻辑的并发性能完全一致,无额外耗时
  • 从设计上消除了多任务共享写入的场景,彻底解决线程安全问题,不需要引入锁、线程安全集合等额外复杂度
  • 代码逻辑清晰,后续维护成本低

如果一定要在任务执行过程中实时写入共享集合,也可以将List<PatientDTO>替换为线程安全的ConcurrentBag<PatientDTO>再执行写入,但性能和可读性均不如上述方案。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 03:31:01