在Parallel中使用DbSet.Add()发现的一系列多线程问题和解决思路详解
作者:balahoho 发布时间:2023-02-13 19:31:10
发现问题
需求很简单,大致就是要批量往数据库写数据,于是打算用Parallel并行的方式写入,希望能利用计算机多核特性加快程序执行速度。想的很美好,于是快速撸了类似下面的一串代码:
using (var db = new SmsEntities())
{
Parallel.For(0, 1000, (i) =>
{
db.MemberCard.Add(new MemberCard()
{
CardNo = "NO_" + i.ToString(),
Banlance = 0,
CreateTime = DateTime.Now,
Name = "Test_" + i.ToString(),
Status = 1
});
});
db.SaveChanges();
}
可意外的是竟然无情的报错了:
奇葩的是当我再次刷新的时候异常又不一样了,于是连着刷新好多次,总结出现过的异常有下面这些:
1、 未将对象引用设置到对象的实例。
2、 已添加了具有相同键的项。
3、 集合已修改;可能无法执行枚举操作。
4、 一个 EdmType 不能多次映射到 CLR 类。EdmType“SmsModel.MemberCard”映射了一次以上。
其中1和2是出现最多的,而且所有异常都是出现在Add的时候,各种吃瓜表情~没办法,接着一一断点调试,还是没找出原因,出于进度考虑,换成了另一种方案,也就是用DbSet的AddRange方法。先在Parallel中累加出一个实体List,然后一次性添加到DbSet中,代码演变为:
List<MemberCard> list = new List<MemberCard>();
using (var db = new SmsEntities())
{
var result = Parallel.For(0, 1000, (i) =>
{
list.Add(new MemberCard()
{
CardNo = "NO_" + i.ToString(),
Banlance = 0,
CreateTime = DateTime.Now,
Name = "Test_" + i.ToString(),
Status = 1
});
});
if (result.IsCompleted)
{
db.MemberCard.AddRange(list);
db.SaveChanges();
}
}
然后编译、测试,没问题,就先放着了。
分析问题
第二天到公司心里还在纠结这个问题,于是打开页面输入生成的数据量1000(真实项目中的循环次数是手动输入的),点按钮提交,嗯,又吃瓜般的异常了…:
心想昨天测试都好好的啊(其实昨天输入的是10,心虚脸...),没办法,上断点吧,一看吓一跳:
明明循环1000次,结果只有971条数据,而且里面还有为null的,经过多次调试发现这是一个随机现象,Count是随机的null也是随机的,有时出现有时没有,初步判断这是一个在多线程情况下引发的一个资源调配异常。So,上MSDN看了一下List的介绍,最后面“线程安全”写着:
一切貌似都清楚了,于是打算验证一下结果,加上了锁,测试结果为:
list里面也没有再出现null了,确认是因为多线程安全引起的异常。于是想起昨天那个问题是否也是同样的问题,再上MSDN搜了一下DbContext类和DbSet类,都是这样说的:
接着就给dbcontext上了锁,测试,这次总算如我所料,完美运行。但是不解的是最初那几个异常是如何产生的,List中虽然数量不够也存在为null的对象,但是并没有直接爆出异常。现在只知道是线程问题,再详细的也搞不清楚,有知道的大神还麻烦指点一下。
寻找解决方案并验证结论
也想过用Partitioner分区来做,但是仔细一想,虽然分区内部是单线程,但是区与区之间还是多线程的,如果分的太细也就失去了Parallel的意义,只得另寻出路。还好Framework为我们也提供了一些线程安全的泛型集合(比如ConcurrentBag、ConcurrentQueue等),不过其本质还是用了锁,于是就综合做了一下单线程list、多线程list加锁、多线程ConcurrentBag、多线程ConcurrentQueue的性能对比,结果如下:
循环1000次时:
循环10000次时:
循环100000次时:
得出结论就是,在执行次数超大时用线程安全类型会更慢,在执行次数较少时线程安全类型也没什么优势。
解决问题
最后在经过仔细测试验证和考虑项目实际需求(几乎不可能一次10000)后,去繁从简,回归原始,用最简单直白的写法单线程循环来完成。虽然一番折腾下来还是回到最初,但是这过程中让我发现了意料之外问题,然后找到了原因,然后测试验证,最终得到了最优解决方案。还是那句话,填完坑,你就比之前更强大了!
来源:http://www.cnblogs.com/hohoa/p/6060228.html


猜你喜欢
- 前言我们在实际项目中,除了会碰到一对一的情况,还有一对多的情况,比如一个用户可以有多辆车,而一辆车只能有一个用户等等,今天我们就来一起学习下
- Selenium.WebDriverSelenium WebDriver 是一组开源 API,用于自动测试 Web 应用程序,利用它可以通过
- AlertDialog可以在当前的界面上显示一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽掉其他控件的交互能力,因此AlertD
- 前言在Java 8之前,默认情况下,接口中的所有方法都是公共的和抽象的。但是这一限制在Java 8中被打破了,Java 8允许开发人员在接口
- 关于人脸识别目前的人脸识别已经相对成熟,有各种收费免费的商业方案和开源方案,其中OpenCV很早就支持了人脸识别,在我选择人脸识别开发库时,
- 返回Json实体类属性大小写问题总归上述问题Rt,其实今天开发刚遇到,当时找了半天为啥前台传参后台却接收不到,原来是返回的时候返回小写,但是
- 本文实例讲述了Android实现将一个Activity设置成窗口样式的方法。分享给大家供大家参考,具体如下:1.在res/value文件夹下
- gcc 命令使用 gcc 编译 c语言-c 编译、汇编到目标代码,不进行链接,也就是直接生成目标文件-o 将输出的文件以指定文件名来储存,有
- 概要笔者近期做到对天气预报JSON数据解析,在此小记。天气预报接口:http://wthrcdn.etouch.cn/weather_min
- 加密代码using System;using System.IO;using System.Security.Cryptography;pu
- 前言众所周知,encache是现在最流行的java开源缓存框架,配置简单,结构清晰,功能强大。通过注解 @Cacheable 可以快速添加方
- 偶然发现有小伙伴错误地使用了Collections.emptyList()方法,这里记录一下。她的使用方式是:public void run
- 本文实例讲述了C#移除所有事件绑定的方法。分享给大家供大家参考。具体分析如下:private delegate int DEL_TEST_E
- 熟悉Eclipse的都知道Eclipse经常性的会出现一些莫名其妙的问题,有时候运行的好好的突然重启一下项目就莫名的报错,所以经常会用到cl
- Spring-Context的作用spring-context提供应用程序上下文,这是Spring的依赖注入容器,它可能总是在以某种方式使用
- 一、集合概述数组其实就是一个集合。集合实际上就是一个容器。可以来容纳其它的数据。二、集合在开发中的应用集合是一个容器,是一个载体,可以一次容
- 1.预警需求为了更好的管理商品日期,需要对仓库的商品进行预警管理,对商品的保质期控制在一个范围内提示出来,也可以通过该功能间接的展示出一个商
- 上下文:程序运行需要的环境(外部变量)上下文切换:将之前的程序需要的外部变量复制保存,然后切换到新的程序运行环境系统调用:(用户态陷入操作系
- Parcelable优点:google专门为安卓写的序列化接口性能好,内存开销小,效率高,写起来复杂缺点:各个机型可能有差异,Parcela
- 如何检查一个数组(无序)是否包含一个特定的值?这是一个在Java中经常用到的并且非常有用的操作。同时,这个问题在Stack Overflow