程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 在Parallel中使用DbSet.Add()發現的一系列多線程問題和解決過程,paralleldbset.add

在Parallel中使用DbSet.Add()發現的一系列多線程問題和解決過程,paralleldbset.add

編輯:關於.NET

在Parallel中使用DbSet.Add()發現的一系列多線程問題和解決過程,paralleldbset.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)後,去繁從簡,回歸原始,用最簡單直白的寫法單線程循環來完成。雖然一番折騰下來還是回到最初,但是這過程中讓我發現了意料之外問題,然後找到了原因,然後測試驗證,最終得到了最優解決方案。還是那句話,填完坑,你就比之前更強大了!


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved