在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();
                }

可意外的是居然无情的报错了:安全

奇葩的是当我再次刷新的时候异常又不同了,因而连着刷新好屡次,总结出现过的异常有下面这些:多线程

一、  未将对象引用设置到对象的实例。性能

二、  已添加了具备相同键的项。测试

三、  集合已修改;可能没法执行枚举操做。spa

四、  一个 EdmType 不能屡次映射到 CLR 类。EdmType“SmsModel.MemberCard”映射了一次以上。线程

其中1和2是出现最多的,并且全部异常都是出如今Add的时候,各类吃瓜表情~没办法,接着一一断点调试,仍是没找出缘由,出于进度考虑,换成了另外一种方案,也就是用DbSet的AddRange方法。先在Parallel中累加出一个实体List,而后一次性添加到DbSet中,代码演变为:3d

            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(真实项目中的循环次数是手动输入的),点按钮提交,嗯,又吃瓜般的异常了…:code

心想昨天测试都好好的啊(其实昨天输入的是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次时:

 

  • 得出结论就是,在执行次数超大时用线程安全类型会更慢,在执行次数较少时线程安全类型也没什么优点。
  • List和DbSet是非线程安全的。

 

解决问题

最后在通过仔细测试验证和考虑项目实际需求(几乎不可能一次10000)后,去繁从简,回归原始,用最简单直白的写法单线程循环来完成。虽然一番折腾下来仍是回到最初,可是这过程当中让我发现了意料以外问题,而后找到了缘由,而后测试验证,最终获得了最优解决方案。仍是那句话,填完坑,你就比以前更强大了!

相关文章
相关标签/搜索