在Parallel中使用DbSet.Add()发现的一系列多线程问题和解决思路详解_C#教程

发现问题

需求很简单,大致就是要批量往数据库写数据,于是打算用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)后,去繁从简,回归原始,用最简单直白的写法单线程循环来完成。虽然一番折腾下来还是回到最初,但是这过程中让我发现了意料之外问题,然后找到了原因,然后测试验证,最终得到了最优解决方案。还是那句话,填完坑,你就比之前更强大了!

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索多线程
parallel
c 多线程并行parallel、易语言大漠多线程思路、dbset、dbset详解、c dbset,以便于您获取更多的相关知识。

时间: 2024-09-20 14:41:42

在Parallel中使用DbSet.Add()发现的一系列多线程问题和解决思路详解_C#教程的相关文章

JS中artdialog弹出框控件之提交表单思路详解_javascript技巧

artDialog是一个基于javascript编写的对话框组件,它拥有精致的界面与友好的接口. 前言: 自适应内容 artDialog的特殊UI框架能够适应内容变化,甚至连外部程序动态插入的内容它仍然能自适应,因此你不必去考虑消息内容尺寸使用它.它的消息容器甚至能够根据宽度让文本居中或居左对齐--这一切全是XHTML+CSS原生实现. 完善的接口 它的接口完善,可以轻易与外部程序配合使用.如异步写入消息.控制位置.尺寸.显示与隐藏.关闭等. 细致的体验 如果不是在输入状态,它支持Esc快捷键关

C#中abstract的用法详解_C#教程

abstract可以用来修饰类,方法,属性,索引器和时间,这里不包括字段. 使用abstrac修饰的类,该类只能作为其他类的基类,不能实例化,而且abstract修饰的成员在派生类中必须全部实现,不允许部分实现,否则编译异常. 如: using System; namespace ConsoleApplication8 { class Program { static void Main(string[] args) { BClass b = new BClass(); b.m1(); } }

C#中const 和 readonly 修饰符的用法详解_C#教程

1. 只有C#内置类型(int,double,long等)可以声明为const;结果.类和数组不能声明为const. 2. readonly 是在字段上使用的修饰符,直接以类名.字段访问. 3. const 必须在申明中初始化.之后不能再修改. 4. readonly可以在申明中初始化,也可以在构造函数中初始化,其它情况不能修改. namespace const_and_readonly { class Program { static void Main(string[] args) { Co

setcookie中Cannot modify header information-headers already sent by错误的解决方法详解_php技巧

复制代码 代码如下: <?php   setcookie("username","bu",time()+3600);   echo "aaaaa";?> 运行有警告Warning: Cannot modify header information - headers already sent by  下面是别人建议 方法一:在PHP里Cookie的使用是有一些限制的.1.使用setcookie必须在<html>标签之前2.

c#中SqlTransaction——事务详解_C#教程

事务处理基本原理      事务是将一系列操作作为一个单元执行,要么成功,要么失败,回滚到最初状态.在事务处理术语中,事务要么提交,要么中止.若要提交事务,所有参与者都必须保证对数据的任何更改是永久的.不论系统崩溃或是发生其他无法预料的事件,更改都须是持久的.只要有一个参与者无法做出此保证,整个事务就会失败.事务范围内的所有数据更改将回滚到特定设置点.   事务将多个操作紧密联系到一起,这样就能保证有联系的两种操作的一致性.以及数据的完整性.举个简单例子:公司的员工信息管理系统,现在要录入数据,

C#中构造函数和析构函数用法实例详解_C#教程

本文实例讲述了C#中构造函数和析构函数用法.分享给大家供大家参考,具体如下: 构造函数与析构函数是一个类中看似较为简单的两类函数,但在实际运用过程中总会出现一些意想不到的运行错误.本文将较系统的介绍构造函数与析构函数的原理及在C#中的运用,以及在使用过程中需要注意的若干事项. 一.构造函数与析构函数的原理 作为比C更先进的语言,C#提供了更好的机制来增强程序的安全性.C#编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙.但是程序通过了编译检查并不表示错误

C#中实现线程同步lock关键字的用法详解_C#教程

1. lock关键字保证一个代码块在执行的过程中不会受到其他线程的干扰,这是通过在该代码块的运行过程中对特定的对象加互斥锁来实现的. 2. lock关键字的参数必须是引用类型的对象.lock对基本数据类型如int,long等无效,因为它所作用的类型必须是对象.如果传入long类型数据,势必被转换为Int64结构类型,则加锁的是全新的对象引用.如果需要对它们进行互斥访问限制,可以使用System.Threading.Interlocked类提供的方法,这个类是提供原子操作的. 3. lock(th

C#中委托(Delegates)的使用方法详解_C#教程

1. 委托是什么? 其实,我一直思考如何讲解委托,才能把委托说得更透彻.说实话,每个人都委托都有不同的见解,因为看问题的角度不同.个人认为,可以从以下2点来理解:  (1) 从数据结构来讲,委托是和类一样是一种用户自定义类型.  (2) 从设计模式来讲,委托(类)提供了方法(对象)的抽象. 既然委托是一种类型,那么它存储的是什么数据? 我们知道,委托是方法的抽象,它存储的就是一系列具有相同签名和返回回类型的方法的地址.调用委托的时候,委托包含的所有方法将被执行. 2. 委托类型的定义 委托是类型

C++中声明类的class与声明结构体的struct关键字详解_C 语言

classclass 关键字声明类类型或定义类类型的对象. 语法 [template-spec] class [ms-decl-spec] [tag [: base-list ]] { member-list } [declarators]; [ class ] tag declarators; 参数 template-spec 可选模板说明. ms-decl-spec 可选存储类说明有关更多信息 tag 给定于类的类型名称.在类范围内的标记成为了保留字.标志是可选项.如果省略,定义匿名类. b