SemaphoreSlim
.NET 4.0 の新しいクラスを使用して、無限に実行できるペースの速いループをレート制限しようとしています。これを単体テストしたところ、ループが十分にタイトで並列度が十分に高い場合、 を呼び出すと、最初にプロパティをチェックしてセマフォ インスタンス自体をロックしてもSemaphoreSlim
、キャッチ不能な例外がスローされることがわかりました。チェックカウント/リリースシーケンス全体。Release()
.Count
この例外により、アプリが停止します。私が知る限り、それを捕まえることはありません。
より深く掘り下げると、呼び出し中に内部的にSemaphoreSlim
独自のプロパティにアクセスしようとしていることがわかりました。インスタンス自体にアクセスするのではなく、例外がスローされます。(これを発見するには、Debug->Exceptions->Common Language Runtime Exceptions->Thrown all checked in Visual Studio でデバッグする必要がありました。実行時にキャッチすることはできません。詳細については、キャッチできない例外を参照してください。).AvailableWaitHandle
Release()
SemaphoreSlim
私の質問は、そのような場合にアプリがすぐに終了する危険を冒さずに、このクラスを使用する防弾方法を知っている人はいますか?
注: セマフォ インスタンスは、RateGate インスタンスにラップされています。コードは、記事「Better Rate Limiting in .NET 」に記載されています。
更新: 再現する完全なコンソール アプリ コードを追加しています。どちらの回答も解決に貢献しました。説明については、以下を参照してください。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Linq;
using System.Text;
using System.Threading;
using PennedObjects.RateLimiting;
namespace RateGateForceTerminateDemo
{
class Program
{
static int _secondsToRun = 10;
static void Main(string[] args) {
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
OptimizeMaxThreads();
Console.WriteLine();
Console.WriteLine("Press any key to exit.");
Console.ReadKey(true);
}
static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) {
Console.WriteLine("Unhandled exception, terminating={0}:{1}", e.IsTerminating, e.ExceptionObject.ToString());
Console.WriteLine("Press any key to terminate app.");
Console.ReadKey(true);
}
static void OptimizeMaxThreads() {
int processors = Environment.ProcessorCount;
int processorsSq = Convert.ToInt32(Math.Pow(processors,2));
int threads = 1;
double result;
Tuple<int, double> maxResult = new Tuple<int, double>(threads, 0);
while (threads <= processorsSq) {
Console.WriteLine("Running for {0}s with upper limit of {1} threads... ", _secondsToRun, threads);
result = TestThrottling(10000000, threads, _secondsToRun);
Console.WriteLine("Ok. Result is {0:N0} ops/s", result);
Console.WriteLine();
if(result > maxResult.Item2)
maxResult = new Tuple<int, double>(threads, result);
threads *= 2;
}
Console.WriteLine("{0} threads achieved max throughput of {1:N0}", maxResult.Item1, maxResult.Item2);
}
static double TestThrottling(int limitPerSecond, int maxThreads, int maxRunTimeSeconds) {
int completed = 0;
RateGate gate = new RateGate(limitPerSecond, TimeSpan.FromSeconds(1));
ParallelLoopResult res = new ParallelLoopResult();
ParallelOptions parallelOpts = new ParallelOptions() { MaxDegreeOfParallelism = maxThreads };
Stopwatch sw = Stopwatch.StartNew();
try {
res = Parallel.For<int>(0, 1000000000, parallelOpts, () => 0, (num, state, subtotal) =>
{
bool succeeded = gate.WaitToProceed(10000);
if (succeeded) {
subtotal++;
}
else {
Console.WriteLine("Gate timed out for thread {0}; {1:N0} iterations, elapsed {2}.", Thread.CurrentThread.ManagedThreadId, subtotal, sw.Elapsed);
// return subtotal;
}
if (sw.Elapsed.TotalSeconds > maxRunTimeSeconds) {
Console.WriteLine("MaxRunTime expired for thread {0}, last succeeded={1}, iterations={2:N0}, elapsed={3}.", Thread.CurrentThread.ManagedThreadId, succeeded, subtotal, sw.Elapsed);
state.Break();
}
return subtotal;
}, (subtotal) => Interlocked.Add(ref completed, subtotal));
}
catch (AggregateException aggEx) {
Console.WriteLine(aggEx.Flatten().ToString());
}
catch (Exception ex) {
Console.WriteLine(ex);
}
sw.Stop();
double throughput = completed / Math.Max(sw.Elapsed.TotalSeconds, 1);
Console.WriteLine("Done at {0}, finished {1:N0} iterations, IsCompleted={2}, LowestBreakIteration={3:N0}, ",
sw.Elapsed,
completed,
res.IsCompleted,
(res.LowestBreakIteration.HasValue ? res.LowestBreakIteration.Value : double.NaN));
Console.WriteLine();
//// Uncomment the following 3 lines to stop prevent the ObjectDisposedException:
//Console.WriteLine("We should not hit the dispose statement below without a console pause.");
//Console.Write("Hit any key to continue... ");
//Console.ReadKey(false);
gate.Dispose();
return throughput;
}
}
}
そのため、@dtb のソリューションを使用すると、スレッド "a" が_isDisposed
チェックを通過しても、スレッド "a" がヒットする前にスレッド "b" がセマフォを破棄する可能性がありましたRelease()
。ExitTimerCallback メソッドと Dispose メソッドの両方で _semaphore インスタンスの周りにロックを追加することがわかりました。@Peter Ritchieの提案により、セマフォを破棄する前に、タイマーをさらにキャンセルして破棄するようになりました。これら 2 つのことの組み合わせにより、プログラムは完了し、RateGate を例外なく適切に破棄できます。
その入力がなければこれを取得できなかったので、自分で答えたくありません。しかし、完全な回答が得られると StackOverflow はより便利になるため、上記のコンソール アプリで問題なく生き残ったパッチまたは疑似パッチを最初に投稿した人を受け入れます。