3

失敗した統合テストを介して観察している断続的な問題をデバッグしようとしていますが、岩と困難な場所の間で立ち往生しているようです。

どこかで何かが作成され、System.Threading.Tasks.Taskその後失敗し、監視されていないタスク例外が発生します。結果のハンドラーで、タスク ID と失敗の原因となった例外を確認できます。私は苦労して自分のコードを分析し、自分のアドバイスに従い、すべてのタスクに名前を付けましたが、まだ問題のあるタスクを見つけていません。私のコードがそれをまったく作成していないようです。

そこで、Taskコンストラクター自体にブレークポイントを設定してみました。これは、関数ブレークポイント (「System.Threading.Tasks.Task.Task(System.Action)」などの場所) を使用して行うことができます。これは機能し、デバッガーが壊れて、Taskクラスのアセンブリが表示されます。ただし、実際に行う必要があるのは、 の ID を の ID に関連付けて、Task失敗Taskすることです。

そのために、Task.Idプロパティをトレース ポイントに出力してみます。しかし、メソッドが最適化されているため、式を評価できないというメッセージが表示されます。

ということで、ソースでデバッグしてみました。すべてのシンボルと what-have-you をセットアップしましたが、できることはすべて試しましたが、うまくいきません。多くのグーグル検索の後、最新の .NET 4 ではサポートされていないことがわかりました。

この問題を診断する方法について誰か考えがありますか?

4

4 に答える 4

4

OK、私はこの問題を追跡しました。バグの詳細は、私が見つけた方法ほど興味深いものではないかもしれませんが、以下の別のセクションで両方について説明します。

問題

問題のあるコードの一部を次に示します。

private static Task<TSuccessor> ThenImpl<TAntecedent, TSuccessor>(Task<TAntecedent> antecedent, Func<Task<TAntecedent>, Task<TSuccessor>> getSuccessor, CancellationToken cancellationToken, TaskThenOptions options)
{
    antecedent.AssertNotNull("antecedent");
    getSuccessor.AssertNotNull("getSuccessor");

    var taskCompletionSource = new TaskCompletionSource<TSuccessor>();

    antecedent.ContinueWith(
        delegate
        {
            var evenOnFaulted = options.HasFlag(TaskThenOptions.EvenOnFaulted);
            var evenOnCanceled = options.HasFlag(TaskThenOptions.EvenOnCanceled);

            if (antecedent.IsFaulted && !evenOnFaulted)
            {
                taskCompletionSource.TrySetException(antecedent.Exception.InnerExceptions));
            }
            else if ((antecedent.IsCanceled || cancellationToken.IsCancellationRequested) && !evenOnCanceled)
            {
                taskCompletionSource.TrySetCanceled();
            }
            else
            {

このメソッドは、ブログThenで紹介した拡張メソッドをサポートしています。

ブログ投稿の実装に加えて、先行タスクが失敗した場合でも、「その後継続」と呼ばれる機能を最近追加しました。

Task.Factory.StartNew(() => { throw new InvalidOperationException(); })
    .Then(() => Console.WriteLine("Executed"), TaskThenOptions.EvenOnFaulted);

これにより、初期例外が無視され、コンソールに「実行済み」が出力されます。ただし、問題は、ThenImpl元の例外を観察していないことです。そのために、次の行を変更しました。

if (antecedent.IsFaulted && !evenOnFaulted)

これに:

if (antecedent.Exception != null && !evenOnFaulted)

そして今、私は問題を理解していません。

さて、なぜこれを追跡するのがまったく困難だったのか疑問に思うかもしれません. 問題は、高度なシナリオを容易にするタスク構成方法がたくさんあるということです。これは、結果として得られる電力のアイデアを提供するための実際のスニペットです。

private Task OnConnectAsync(CancellationToken cancellationToken, object state)
{
    var firstAttempt = true;
    var retryOnFailureTask = TaskUtil
        .RetryOnFailure(
                () => TaskUtil.Delay(firstAttempt ? TimeSpan.Zero : this.reconnectDelay, cancellationToken)
                .Then(
                    x =>
                    {
                        if (!firstAttempt)
                        {
                            Interlocked.Increment(ref this.connectionAttempts);
                        }

                        firstAttempt = false;
                    })
                .Then(x => this.loggerService.Debug("Attempting to connect communications service (attempt #{0}).", this.connectionAttempts), cancellationToken)
                .Then(x => this.communicationsService.ConnectAsync(cancellationToken), cancellationToken)
                .Then(x => this.loggerService.Debug("Successfully connected communications service (attempt #{0}).", this.connectionAttempts), cancellationToken)
                .Then(x => this.communicationsService.AuthenticateAsync(cancellationToken), cancellationToken)
                .Then(x => this.loggerService.Debug("Successfully authenticated communications service (attempt #{0}).", this.connectionAttempts), cancellationToken)
                .Then(x => this.ReviveActiveStreamsAsync(cancellationToken), cancellationToken)
                .Then(x => this.loggerService.Debug("Successfully revived streams (attempt #{0}).", this.connectionAttempts), cancellationToken),
            null,
            cancellationToken);

    return retryOnFailureTask;
}

カスタムRetryOnFailureThen、およびDelayメソッドに注意してください。それは私が話していることの良い味です。

もちろん、これの欠点は、問題が発生したときに追跡することです。この点に関して、TPL は不十分な仕事をしていると感じずにはいられません。私の考えでは、それぞれTaskに作成者に関する情報が含まれている必要があります。少なくとも、TaskCreated開発者が独自のデバッグ情報でタスクを補足できるように、TPL (イベントなど) にフックが必要です。.NET 4.5 で状況が改善された可能性がありますが、私は .NET 4.0 を使用しています。

メソッド

問題を追跡するための鍵は、補足メッセージで例外をラップする でTask作成したそれぞれを苦労してラップすることでした。TaskCompletionSourceたとえば、これはToBooleanTask私が事前に持っている拡張メソッドです:

public static Task<bool> ToBooleanTask(this Task task)
{
    var taskCompletionSource = new TaskCompletionSource<bool>();

    task.ContinueWith(
        x =>
        {
            if (x.IsFaulted)
            {
                taskCompletionSource.TrySetException(x.Exception.GetBaseException());
            }
            else if (x.IsCanceled)
            {
                taskCompletionSource.TrySetCanceled();
            }
            else
            {
                taskCompletionSource.TrySetResult(true);
            }
        });

    return taskCompletionSource.Task;
}

そして、これがこの変更を行った後のものです:

public static Task<bool> ToBooleanTask(this Task task)
{
    var taskCompletionSource = new TaskCompletionSource<bool>();

    task.ContinueWith(
        x =>
        {
            if (x.IsFaulted)
            {
                taskCompletionSource.TrySetException(new InvalidOperationException("Failure in to boolean task", x.Exception.GetBaseException()));
            }
            else if (x.IsCanceled)
            {
                taskCompletionSource.TrySetCanceled();
            }
            else
            {
                taskCompletionSource.TrySetResult(true);
            }
        });

    return taskCompletionSource.Task;
}

この場合、私はすでに を持っているTaskCompletionSourceので、簡単でした。それ以外の場合は、明示的に を作成し、基にTaskCompletionSourceなる からの障害/キャンセル/結果を に転送する必要がありTaskましたTaskCompletionSource

余談ですが、ToBooleanTask拡張メソッドの使用について不思議に思うかもしれません。汎用タスクと非汎用タスクの両方を処理する単一のメソッドを実装したい場合に非常に便利です。ジェネリック バージョンを実装してから、非ジェネリック オーバーロードを呼び出しToBooleanTaskてジェネリック タスクを作成し、それをジェネリック オーバーロードに渡すことができます。

考えられるすべての原因を調べて上記のように補足したら、テストが失敗するまでテストを再実行し、それToBooleanTaskが観察されていないタスクを実際に作成していることに気付きました。したがって、次のように変更しました。

public static Task<bool> ToBooleanTask(this Task task)
{
    var stackTrace = new System.Diagnostics.StackTrace(true);
    var taskCompletionSource = new TaskCompletionSource<bool>();

    task.ContinueWith(
        x =>
        {
            if (x.IsFaulted)
            {
                taskCompletionSource.TrySetException(new InvalidOperationException("Failure in to boolean task with stack trace: " + stackTrace, x.Exception.GetBaseException()));
            }
            else if (x.IsCanceled)
            {
                taskCompletionSource.TrySetCanceled();
            }
            else
            {
                taskCompletionSource.TrySetResult(true);
            }
        });

    return taskCompletionSource.Task;
}

これにより、障害が発生したときに完全なスタック トレースが得られます。失敗するまでテストを再実行しました。- 問題を追跡するために必要な情報を入手しました:

Failure in to boolean task with stack trace:    at XXX.Utility.Tasks.TaskExtensions.ToBooleanTask(Task task) in C:\XXX\Src\Utility\Tasks\TaskExtensions.cs:line 110
   at XXX.Utility.Tasks.TaskExtensions.Then(Task antecedent, Func`2 getSuccessor, CancellationToken cancellationToken, TaskThenOptions options) in C:\XXX\Src\Utility\Tasks\TaskExtensions.cs:line 199
   at XXX.Utility.Tasks.StateMachineTaskFactory`1.TransitionTo(T endTransitionState, CancellationToken cancellationToken, WaitForTransitionCallback`1 waitForTransitionCallback, ValidateTransitionCallback`1 validateTransitionCallback, PreTransitionCallback`1 preTransitionCallback, Object state) in C:\XXX\Src\Utility\Tasks\StateMachineTaskFactory.cs:line 312
   <snip>

Thenしたがって、それがを呼び出すオーバーロードの 1 つであることがわかりましたToBooleanTask。その後、その正確なコードをたどることができ、問題はすぐに明らかになりました。

しかし、これは私に興味をそそられました。各タスクに名前を付けて補足するという当初のアプローチでは、なぜ結果が得られなかったのでしょうか? 修正を元に戻し、 によって作成されたタスクに直接名前を付け、ToBooleanTask失敗するまで再実行しようとしました。案の定、デバッガーにタスク名が表示されました。明らかに、もともとこのタスクに名前を付けるのを忘れていました。

ふぅ!

于 2012-11-20T15:40:19.650 に答える
1

タスクの数が管理可能な場合は、Visual Studio の「オブジェクト ID の作成」機能を使用して、各タスクを追跡できます。

  • タスク コンストラクターのブレークポイントで、タスクを [ウォッチ] ウィンドウに配置します。
  • ウォッチ ウィンドウでタスクを右クリックし、[オブジェクト ID の作成] を選択します。これにより、値の末尾に 1# が追加されることに注意してください。これをタスクごとに行います。
  • ワークフローを実行します。例外をスローするタスクで、その番号を確認します。
于 2012-11-19T15:00:49.120 に答える
0

可能であれば、Task を作成するコードを変更して、オブジェクトを受け取る Task Constructor を使用することができます。 Task(Action<Object>, Object)

次に、タスクを作成する各場所で、一意のもの (識別整数、コール スタックなど) を渡すことができます。

次に、UnobservedTaskException で、この日付 (に格納されている) を調べることができますTask.AsyncState

これは、それが自分のタスクか他のタスクかを絞り込むのに役立ちます。

于 2012-11-20T14:22:56.260 に答える
0

イベントを中断し、UnobservedTaskExceptionのプライベート フィールドを調べますTask。プライベート フィールドを含むクラスTaskによってイベントが発生しているため、コール スタックで 1 ~ 2 レベル上を見つけることができます。TaskExceptionHolderm_task

Taskオブジェクトには、実行の一部として実行されたアクションが含まれます。

于 2012-11-19T11:37:29.253 に答える