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;
}
カスタムRetryOnFailure
、Then
、および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
失敗するまで再実行しようとしました。案の定、デバッガーにタスク名が表示されました。明らかに、もともとこのタスクに名前を付けるのを忘れていました。
ふぅ!