如何在Entity Framework Core 6.0中用Parallel Loop更新元素列表
解决EF Core并行更新DbContext引发的并发安全错误
错误原因
EF Core的DbContext实例是非线程安全的,其内部用于跟踪实体状态的集合不支持并发修改。你用Parallel.ForEach多线程同时调用context.Update(item),会导致多个线程同时修改DbContext的内部状态集合,最终触发状态损坏的报错。
解决方案
方案1:EF Core 6.0批量更新(推荐,性能最优)
因为EF Core 6.0无内置批量更新API,推荐以下两种批量处理方式:
方式A:使用第三方批量操作库
先安装NuGet包:Install-Package EFCore.BulkExtensions
using (var context = new ApplicationDbContext()) { // 筛选并设置需要更新的实体状态 var inStockItems = await context.Items .Where(i => Helper.checkList.Contains(i.Link)) .ToListAsync(); foreach (var item in inStockItems) { item.StatusId = (int)Emuns.Status.InStock; } var outStockItems = await context.Items .Where(i => !Helper.checkList.Contains(i.Link)) .ToListAsync(); foreach (var item in outStockItems) { item.StatusId = (int)Emuns.Status.OutStock; } // 批量提交更新 await context.BulkUpdateAsync(inStockItems); await context.BulkUpdateAsync(outStockItems); }
方式B:手动分批次更新(无需第三方库)
将数据分批次加载并更新,避免一次性加载6万条数据占用过多内存:
using (var context = new ApplicationDbContext()) { int batchSize = 1000; int totalCount = await context.Items.CountAsync(); int batchCount = (int)Math.Ceiling((double)totalCount / batchSize); for (int i = 0; i < batchCount; i++) { var batchItems = await context.Items .Skip(i * batchSize) .Take(batchSize) .ToListAsync(); foreach (var item in batchItems) { item.StatusId = Helper.checkList.Contains(item.Link) ? (int)Emuns.Status.InStock : (int)Emuns.Status.OutStock; } await context.SaveChangesAsync(); context.ChangeTracker.Clear(); // 清理跟踪对象,释放内存 } }
方案2:并行处理+独立DbContext(不推荐,性能差)
如果必须使用并行循环,需为每个线程创建独立的DbContext实例,避免多线程共享同一个非线程安全上下文:
var itemList = await context.Items.ToListAsync(); Parallel.ForEach(itemList, (item) => { using (var threadContext = new ApplicationDbContext()) { var dbItem = threadContext.Items.Find(item.Id); if (dbItem != null) { dbItem.StatusId = Helper.checkList.Contains(item.Link) ? (int)Emuns.Status.InStock : (int)Emuns.Status.OutStock; threadContext.SaveChanges(); } } });
该方式会产生6万次数据库请求,性能极低,仅在特殊场景下考虑使用。
方案3:并行分块处理(优化方案2性能)
将数据分块,每个块使用独立DbContext处理,减少DbContext创建次数和数据库请求次数:
var itemList = await context.Items.ToListAsync(); int chunkSize = 100; var chunks = itemList.Chunk(chunkSize); // .NET 6+支持Chunk方法 Parallel.ForEach(chunks, (chunk) => { using (var threadContext = new ApplicationDbContext()) { foreach (var item in chunk) { var dbItem = threadContext.Items.Find(item.Id); if (dbItem != null) { dbItem.StatusId = Helper.checkList.Contains(item.Link) ? (int)Emuns.Status.InStock : (int)Emuns.Status.OutStock; } } threadContext.SaveChanges(); // 每块批量保存 } });
总结
优先选择批量更新方案,既能避免线程安全问题,又能保证更新效率。并行循环方案仅在无法使用批量更新的特殊场景下考虑,且需严格保证每个线程使用独立DbContext。
内容的提问来源于stack exchange,提问作者pupy frias
相关产品推荐
相关产品推荐

