多数の BackgroundWorker スレッドを作成するコードがいくつかあり、それぞれがいくつかのデータベース処理を行います。時々、それらのスレッドが例外をスローします (通常はタイムアウトが原因です。これは最近のことであり、私はそれを理解しなければならない人ではありません)。
いずれかのスレッドが失敗すると、操作全体が役に立たなくなり、すべてが 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 がワーカー スレッドで呼び出されたと想定していたため、ロックをかけました (通常はそうではありませんが、これは Web サービスであり、Windows アプリケーション外の動作は異なるようです)。
例外は、List.Add がスレッド 1 によって呼び出され、次にスレッド 2 によって呼び出されたように見えますが、最初の呼び出しがまだ実行中であり、オブジェクトはまだ一貫性のない状態にあります。複数のスレッドがデフォルトの 30 秒の SqlCommand タイムアウトに達したことが原因で (実際には、これまでのところ) 複数の障害が常に発生するため、それらはほぼ同時にそれを実行します。そして、リストにロックがない場合、小さなテストアプリでその動作を正確に再現できます。
Add() 呼び出し中に exAsync.Count または exAsync.First() にアクセスしているため、ちょうどいいタイミングで Add の前に ctThreadsFinished をインクリメントして待機ループを通過している可能性がありますか? それは Add() を壊すことができますか? 共有ロック オブジェクトを持ち、待機ループ内のカウンター アクセスと最後のビットにロックを配置することは確かに賢明でした。
ただし、exAsync にアクセスするすべてのものが実際にはメイン スレッドで実行されていない場合でも、Add() 呼び出しの周りに lock() ブロックがあります。私の最初の衝動は、List を System.Collections.Concurrent.ConcurrentBag に置き換えることでしたが、それで問題が解決すると信じる特別な理由はありません。
これは誰にとっても意味がありますか?