7

ホスティング プロセスがクラッシュしないように、タスクで使用できる C# 拡張メソッドを使用して、スローされた例外が最小限に抑えられるようにします。.NET4.5 では、動作がわずかに変更されたため、これは発生しませんが、監視されていない例外イベントは引き続きトリガーされます。ここでの私の課題は、拡張メソッドが機能することを証明するテストを作成することです。私は NUnit テスト フレームワークを使用しており、ReSharper がテスト ランナーです。

私が試してみました:

var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(() =>
                          {
                              throw new NaiveTimeoutException();
                              return new DateTime?();
                          });
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);

でのテストは常に失敗しますAssert.IsTrue。このテストを LINQPad などで手動で実行すると、期待どおりの動作がwasUnobservedException返されtrueます。

TaskScheduler.UnobservedTaskExceptionテストフレームワークが例外をキャッチし、それがトリガーされないように監視していると推測しています。

次のようにコードを変更してみました。

var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(async () =>
                          {
                              await TaskEx.Delay(5000).WithTimeout(1000).Wait();
                              return new DateTime?();
                          });
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);

このコードで試みたのは、例外がスローされる前にタスクが GC されるようにすることでした。これにより、ファイナライザーはキャッチされず、監視されていない例外を確認できます。ただし、これにより、上記と同じ失敗が発生しました。

実際、テスト フレームワークによって何らかの例外ハンドラが接続されているのでしょうか。もしそうなら、それを回避する方法はありますか?それとも、私は何かを完全に台無しにしていて、これを行うためのより良い/簡単/クリーンな方法がありますか?

4

2 に答える 2

10

このアプローチにはいくつかの問題があります。

まず、明確な競合状態があります。タスクが返されたときTaskEx.Run、タスクはスレッド プールへの要求をキューに入れただけです。タスクは必ずしも完了していません。

次に、いくつかのガベージ コレクターの詳細に遭遇しています。デバッグでコンパイルすると、実際には、コンパイラがそのように感じるときはいつでも、ローカル変数 (つまりres) の有効期間はメソッドの最後まで延長されます。

これら 2 つの問題を念頭に置いて、次のコードを渡すことができました。

var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = Task.Run(() =>
{
    throw new NotImplementedException();
    return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);

ただし、まだ 2 つの問題があります。

(技術的には)まだ競合状態があります。UnobservedTaskExceptionタスクファイナライザーの結果として発生しますが、タスクファイナライザーから発生するという保証はありませ。現在、そうであるように見えますが、それは非常に不安定な解決策のように思えます (制限されたファイナライザーがどのように想定されているかを考えると)。UnobservedTaskExceptionしたがって、フレームワークの将来のバージョンでは、ファイナライザーが を直接実行するのではなく、スレッド プールのキューに入れるだけであることを知ってもあまり驚かないでしょう。その場合、タスクがファイナライズされるまでにイベントが処理されているという事実に依存することはできなくなります (上記のコードによって暗黙的に想定されます)。

UnobservedTaskException単体テスト内のグローバル状態 ( ) の変更に関する問題もあります。

これらの問題の両方を考慮に入れると、最終的には次のようになります。

var mre = new ManualResetEvent(initialState: false);
EventHandler<UnobservedTaskExceptionEventArgs> subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
    var res = Task.Run(() =>
    {
        throw new NotImplementedException();
        return new DateTime?();
    });
    ((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
    res = null; // Allow the task to be GC'ed
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (!mre.WaitOne(10000))
        Assert.Fail();
}
finally
{
    TaskScheduler.UnobservedTaskException -= subscription;
}

これも合格ですが、その複雑さを考えると、かなり疑わしい価値があります。

于 2014-01-21T21:09:43.503 に答える