2013-11-20 17 views
0

我們有一些代碼可以創建一些BackgroundWorker線程,每個線程都執行一些數據庫工作。有時候這些線程會拋出一個異常(通常是由於超時 - 這是最近發生的事情,我不是那個必須弄清楚的人)。BackgroundWorker.RunWorker在List.Add中發生異常

如果任何線程失敗,整個操作就沒用了,整個事情都發生在Web服務調用中。因此,如果失敗,我們需要在主線程中拋出一個異常,該異常將被捕獲並轉換爲客戶端的SOAP錯誤異常。

我們收集列表中的線程例外。在幾十次的代碼中,多達7個工作線程都在同一時間拋出異常,有一次List在System.Collections.Generic.List`1.Add(T item)中拋出異常:

System.IndexOutOfRangeException 

Message: Index was outside the bounds of the array. 

這裏的,大致代碼:

// Collect Exceptions thrown by async calls. 
var exAsync = new List<Exception>(); 
int ctThreadsFinished = 0; 
int ctThreadsBegun = 0; 

Action<Exception> handleException = (ex) => { 
    lock(exAsync) { 
     ++ctThreadsFinished; 
     exAsync.Add(ex); 
    } 
}; 

// ...create and run multiple BackgroundWorker threads, incrementing 
// ctThreadsBegun for each thread. They will ++ctThreadsFinished on 
// successful completion. That part works. 

// If a thread throws an exception, its RunWorkerCompleted event will pass the 
// exception to handleException. 

while (ctThreadsFinished < ctThreadsBegun) 
{ 
    System.Threading.Thread.Sleep(100); 
} 

if (exAsync.Count == 1) 
{ 
    throw new Exception(exAsync.First().Message, exAsync.First()); 
} 
else if (exAsync.Count > 1) 
{ 
    var msg = String.Join("\n", exAsync.Select(ex => ex.Message)); 
    throw new AggregateException(msg, exAsync); 
} 

我把鎖就可以了,因爲我曾以爲,RunWorkerCompleted是在工作線程(稱爲其normally it isn't,但是這是Web服務,它看起來如behavior outside a Windows application will differ)。

例外看起來像像List.Add由線程1調用,然後在第一個調用仍在執行並且該對象仍處於不一致狀態時由線程2調用。由於多個線程會觸發默認的30秒SqlCommand超時,因此總是(實際上到目前爲止)出現多個失敗,他們將在同一時間執行此操作。如果在列表上沒有鎖定,我可以在一個小測試應用程序中重新創建該行爲,

難道它是在加入之前增加ctThreadsFinished以恰好恰當的時刻來越過等待循環,所以它在Add()調用期間訪問exAsync.Count或exAsync.First()?可以破解Add()嗎?擁有一個共享鎖對象並且在等待循環中的計數器訪問周圍放置鎖定,並在末尾放置位,這當然是明智的。

但是,即使訪問exAsync的所有內容在主線程中都沒有這樣做,但在Add()調用周圍還是有一​​個lock()塊。我的第一個衝動是用System.Collections.Concurrent.ConcurrentBag替換List,但我沒有特別的理由相信會解決這個問題。

這對任何人都有意義嗎?

回答

1

只鎖定Add不會解決問題;只是確保兩個不同的Add調用不會相互干擾。您在Add被調用之前完成的等待循環結束時識別的競態條件是有效的,並且會導致您遇到的問題。您還應該鎖定檢查exAsync的整個if/else塊。

您不應該用ConcurrentBag替換列表,因爲您可能會遇到不同的問題:在將最後一個異常插入列表之前從包中讀取數據。

(編輯)我也會使用ManualResetEventSlim來阻塞線程而不是睡眠循環。你可以讓主線程等待它,當計數到0時,最後一個工作線程會發出信號。

此外,最好創建一個私有對象並鎖定它,而不是列表本身。這樣你可以明確你正在同步什麼。

1

問題在於使用鎖定語句的方式。從this後一個報價:

最後,有一個鎖(本)實際修改作爲參數傳遞的對象的常見的誤解,並以某種方式爲只讀或無法使。這是錯誤的。作爲參數傳遞給對象的對象僅僅作爲一個關鍵。如果該鍵上已經有一個鎖,則不能進行鎖定;否則,鎖定被允許。

「鎖定」您的列表不會阻止其他代碼訪問該對象。它只是說,沒有人可以使用列表作爲關鍵字來創建鎖。一個ConcurrentBag應該修復你的異常,但是如果你的拋出異常代碼在你的最後一個句柄完成之前被擊中,那麼向列表中添加一個異常會引入你錯過最後一個異常的可能性。

+0

對,我鎖定的對象是任意的。可能是列表,可能是我母親在Unicode中的孃家姓。它鎖定了一段代碼,而不是列表本身:http://msdn.microsoft.com/en-us/library/c5kehkcz(v=vs.110).aspx我期望它工作,因爲exAsync.Add()是隻在該代碼段中調用,不是因爲我使用exAsync作爲鎖定標記。 –

+0

是的,但後來訪問列表以檢查異常的代碼不在鎖內,所以即使最後一個異常被另一個線程添加,它也會高興地訪問您的列表。 – Jason

+0

是的,的確如此。 –