Parallel.ForEach 在 I/O 密集型导入中变慢,因线程争抢连接池或锁;应限流(如 MaxDegreeOfParallelism=4–8)、改用 SqlBulkCopy(设 BatchSize、EnableStreaming、TABLOCK)或分批 SaveChanges(禁用自动追踪、每 500–2000 条提交),并用 SemaphoreSlim 控制并发防连接池耗尽。
Parallel.ForEach 处理大批量数据导入时为什么反而变慢?不是所有“并发”都加速,Parallel.ForEach 在 I/O 密集型场景(如数据库插入、文件读写)中常因线程争抢连接或锁而拖慢整体吞吐。它默认按 CPU 核心数分配线程,但数据库连接池、磁盘 IO 或网络带宽才是真实瓶颈。
DbConte
xt.SaveChanges() 或 SqlCommand.ExecuteNonQuery() —— 每次调用都可能触发独立事务和连接获取ExecuteSqlRaw + 参数化 SQL 批量插入,或 Dapper 的 connection.Execute(sql, list)
Parallel.ForEach(list, new ParallelOptions { MaxDegreeOfParallelism = 4 }, item => { ... }),值设为数据库连接池大小(如 SQL Server 默认 100,实际建议 4–8)SqlBulkCopy 真正跑满带宽?SqlBulkCopy 是 .NET 原生最快的数据导入方式,但默认配置下常只用单线程、小缓冲、无索引优化,导致吞吐远低于理论值。
BatchSize(如 10000),避免单次提交过大内存溢出或过小频繁往返EnableStreaming = true,配合 DataTable 或 IDataReader 流式供数,减少内存峰值ALTER TABLE ... DISABLE TRIGGER 和 DROP INDEX(完事后重建),尤其对有唯一约束或外键的表影响巨大WITH (TABLOCK) 提示(通过 SqlBulkCopy.SqlRowsCopied 事件无法控制,需在 SQL 层显式加)SaveChanges 卡住的三个常见原因EF Core 的变更跟踪机制在海量数据下会吃光内存、拖慢性能,SaveChanges 不是“越快越好”,而是“越少调用越好”。
context.ChangeTracker.AutoDetectChangesEnabled = false,手动 context.Entry(entity).State = EntityState.Added
SaveChanges(),而非全量后一次提交(否则事务日志暴涨、锁表时间过长)AddRange 直接传大集合——它仍会逐个标记状态;改用 context.AddRange(entities.Take(batchSize)) + 循环用 await context.SaveChangesAsync() 配合 Task.WhenAll 看似高效,实则极易触发 Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool 错误。
Task.WhenAll 同时发起 100 个 SaveChanges —— 就等于向连接池申请 100 个连接,远超默认上限(通常 100,但受服务器资源限制)SemaphoreSlim 限流,例如 var semaphore = new SemaphoreSlim(8);
await semaphore.WaitAsync();
try { await context.SaveChangesAsync(); }
finally { semaphore.Release(); }
Max Pool Size=200 不解决问题,只是掩盖争抢——应优先降低并发请求数,再调高池大小作为补充SqlBulkCopy、分批 SaveChanges 还是纯原生 ADO.NET,比盲目加并发更有效。