需求很簡單,大體就是要批量往數據庫寫數據,因而打算用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次時:
最後在通過仔細測試驗證和考慮項目實際需求(幾乎不可能一次10000)後,去繁從簡,迴歸原始,用最簡單直白的寫法單線程循環來完成。雖然一番折騰下來仍是回到最初,可是這過程當中讓我發現了意料以外問題,而後找到了緣由,而後測試驗證,最終獲得了最優解決方案。仍是那句話,填完坑,你就比以前更強大了!