当前位置: 首页 > 编程笔记 >

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

农弘毅
2023-03-14
本文向大家介绍在Parallel中使用DbSet.Add()发现的一系列多线程问题和解决思路详解,包括了在Parallel中使用DbSet.Add()发现的一系列多线程问题和解决思路详解的使用技巧和注意事项,需要的朋友参考一下

发现问题

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

 类似资料:
  • 本文向大家介绍如何解决PHP无法实现多线程的问题,包括了如何解决PHP无法实现多线程的问题的使用技巧和注意事项,需要的朋友参考一下 有没有办法在PHP中实现多线程呢?假设你正在写一个基于多台服务器的PHP应用,理想的情况时同时向多台服务器发送请求,而不是一台接一台。可以实现吗?当有人想要实现并发功能时,他们通常会想到用fork或者spawn threads,但是当他们发现PHP不支持多线程的时候,

  • 本文向大家介绍java 多线程饥饿现象的问题解决方法,包括了java 多线程饥饿现象的问题解决方法的使用技巧和注意事项,需要的朋友参考一下 java 多线程饥饿现象的问题解决方法 当有线程正在读的时候,不允许写 线程写,但是允许其他的读线程进行读。有写线程正在写的时候,其他的线程不应该读写。为了防止写线程出现饥饿现象,当线程正在读,如果写线程请求写,那么应该禁止再来的读线程进行读。  实现代码如下

  • 本文向大家介绍vue解决跨域路由冲突问题思路解析,包括了vue解决跨域路由冲突问题思路解析的使用技巧和注意事项,需要的朋友参考一下 vue 简介 Vue.js(读音 /vjuː/, 类似于 view) 是一套构建用户界面的渐进式框架。 Vue 只关注视图层, 采用自底向上增量开发的设计。 Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。 Vue 学习起来非常简单,本教

  • 本文向大家介绍PYTHON 中使用 GLOBAL引发的一系列问题,包括了PYTHON 中使用 GLOBAL引发的一系列问题的使用技巧和注意事项,需要的朋友参考一下 哪里出问题了   python 中,使用 global 会将全局变量设为本函数可用。同时,在函数内部访问变量会先本地再全局。   在嵌套函数中,使用 global 会产生不合常理的行为。   上代码:   而在函数 x() 中,没有用

  • 本文向大家介绍python爬虫中多线程的使用详解,包括了python爬虫中多线程的使用详解的使用技巧和注意事项,需要的朋友参考一下 queue介绍 queue是python的标准库,俗称队列.可以直接import引用,在python2.x中,模块名为Queue。python3直接queue即可 在python中,多个线程之间的数据是共享的,多个线程进行数据交换的时候,不能够保证数据的安全性和一致性

  • 我有一个组织表,部门表和员工表。一个组织有许多部门,一个部门有许多员工。在执行 (组织的 PK)时,我看到 Employee 表也被查询 n 次,其中 n 是组织的部门数。 如何避免在执行相同操作时获取员工数据?我在这里看到了一个N 1问题。但不确定如何在嵌套的一对多映射中避免它。 提取类型设置为“延迟”。