0

多数の 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 に置き換えることでしたが、それで問題が解決すると信じる特別な理由はありません。

これは誰にとっても意味がありますか?

4

2 に答える 2

1

ロックするだけAddでは問題は解決しません。これは、2 つの異なるAdd呼び出しが互いに干渉しないようにするだけです。呼び出される前に終了する待機ループで特定した競合状態Addは有効であり、表示されている問題を引き起こす可能性があります。また、検査している if/else ブロック全体をロックする必要がありますexAsync

別の問題が発生する可能性があるため、リストを a に置き換えるだけではいけませんConcurrentBag。最後の例外がリストに挿入される前にバッグから読み取ることです。

(編集) また、スリープ ループではなく、スレッドをブロックするためにManualResetEventSlimを使用します。メイン スレッドを待機させ、カウントが 0 になったときに最後のワーカー スレッドに通知させることができます。

また、リスト自体ではなく、プライベート オブジェクトを作成してロックすることをお勧めします。そうすれば、同期しているものについて明確にすることができます。

于 2013-11-20T16:49:33.797 に答える
1

問題は、lock ステートメントの使用方法にあります。この投稿からの引用:

最後に、lock(this) がパラメーターとして渡されたオブジェクトを実際に変更し、何らかの方法で読み取り専用またはアクセス不能にするという一般的な誤解があります。これは誤りです。lock にパラメーターとして渡されるオブジェクトは、単にキーとして機能します。そのキーですでにロックが保持されている場合、ロックを作成することはできません。それ以外の場合、ロックは許可されます。

リストを「ロック」しても、他のコードがそのオブジェクトにアクセスするのを防ぐことはできません。リストをキーとして使用してロックを作成することはできません。ConcurrentBag は例外を修正する必要がありますが、最後のハンドルがリストへの例外の追加を完了する前にスロー例外コードがヒットした場合、最後の例外を見逃す可能性が生じます。

于 2013-11-20T16:53:15.923 に答える