IIS 7、.NET 4 で実行されている ASP.NET アプリケーションがあり、別の Web サービス (スタイル通知を起動して忘れる) と通信する必要がある場合があります。これまで、私はThread
for each single 通知を起動していましたが、これはまったく効果がないと思います (新しいスレッドを作成するコストが高いため)。
そのため、BlockingCollection<>
処理が必要な通知を追跡するためにコンテナーを使用し始めApplication_Start
、このコードを使用して 3 つのコンシューマーを開始し、このキューを処理します。
Task.Factory.StartNew(Consumer.Create(sessionBuilder), TaskCreationOptions.LongRunning);
Task.Factory.StartNew(Consumer.Create(sessionBuilder), TaskCreationOptions.LongRunning);
Task.Factory.StartNew(Consumer.Create(sessionBuilder), TaskCreationOptions.LongRunning);
sessionBuilder は、データベース セッションを提供する単なるオブジェクトです。
コンシューマーは次のようになります。
public static class Consumer
{
private static readonly ILog log = LogManager.GetLogger(typeof(Consumer));
public static Action Create(ISessionBuilder sessionBuilder)
{
return () =>
{
while (true)
{
try
{
if (RequestQueue.Requests.IsCompleted)
return;
var request = RequestQueue.Requests.Take();
Process(sessionBuilder, request);
}
catch (Exception e)
{
log.Error("Exception caught in the consumer's thread", e);
}
}
};
}
// The processing methods
//
}
これは問題なく動作しているように見えますが、新しいバージョンのアプリケーションを IIS にデプロイするとすぐに、サーバーの CPU がw3wp
プロセスで 99% 使用され始めます。これは間違いなく BlockingQueue の使用に伴うもので、変更前はこのようにはなっていませんでした。
CPU 時間がどこで費やされているかを正確に把握しようとして、WMemoryProfilerと WinDbg を使用して、すべてのマネージド スレッドのスタック トレースのキャプチャをいつでも確認できるようにしました。
このようにして、プロセスが CPU を 100% 使用している瞬間に、17 個のコンシューマ スレッドが実行されており、それらすべてが次の処理に時間を費やしていることがわかりました。
[GCFrame: 0000000012b2df58]
[HelperMethodFrame_1OBJ: 0000000012b2e048] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
System.Threading.SemaphoreSlim.WaitUntilCountOrTimeout(Int32, Int64, System.Threading.CancellationToken)
System.Threading.SemaphoreSlim.Wait(Int32, System.Threading.CancellationToken)
System.Collections.Concurrent.BlockingCollection`1[[System.__Canon, mscorlib]].TryTakeWithNoTimeValidation(System.__Canon ByRef, Int32, System.Threading.CancellationToken, System.Threading.CancellationTokenSource)
System.Collections.Concurrent.BlockingCollection`1[[System.__Canon, mscorlib]].TryTake(System.__Canon ByRef, Int32, System.Threading.CancellationToken)
System.Collections.Concurrent.BlockingCollection`1[[System.__Canon, mscorlib]].Take()
AppServer.Core.Queue.Consumer+<>c__DisplayClass1.b__0()
System.Threading.Tasks.Task.Execute()
System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef)
System.Threading.Tasks.Task.ExecuteEntry(Boolean)
System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
System.Threading.ThreadHelper.ThreadStart(System.Object)
[GCFrame: 0000000012b2eae8]
[DebuggerU2MCatchHandlerFrame: 0000000012b2eed0]
[ContextTransitionFrame: 0000000012b2f068]
[DebuggerU2MCatchHandlerFrame: 0000000012b2f290]
だから、明らかに、私はここで問題を抱えています。コンシューマーは、AppDomain のアンロード時に終了されません (デプロイ後に最初の HTTP 要求が到着するとすぐに発生します)。
1) 消費者が終了しないのはなぜですか? そのためだと思いAppDomainUnloadException
ました。
2) 通知するグローバル キャンセル トークンを用意する必要があります (ただし、いつですか?) 現在の消費者に時間が終わったことを知らせますか? または、ここで推奨されるパターンは何ですか?
3) 上記の呼び出しスタックで 17 のスレッドが時間を費やしているのは正常ですか? Take()
というか、 onを呼び出すとBlockingCollection<>
、このような状況に同時に多くのスレッドが配置されるのは正常なことでしょうか?