2014年6月26日

Task 成功時のみ ContinueWith

TaskTask<TResult> の成功(完了)時のみ ContinueWith させたいと思ったことはないだろうか。
下記パターンとか・・・
Task(1つ目の処理)
.ContinueWith(成功時の処理)
.ContinueWith(成功時の処理)
・・・
.ContinueWith(エラーをまとめて処理)

TaskContinuationOptions.OnlyOnRanToCompletion を ContinueWith の引数に入れてやれば、成功時のみは可能なのだが、ContinueWith のメソッドチェーンで実装が難しくなる。

var task = Task.Factory.StartNew(() => {
    Console.WriteLine("1st task");
    /* 略 */
    return 0;
}).ContinueWith(t => {
    // 1st task 成功時のみ
    Console.WriteLine("2nd task");
    /* 略 */
    return 0;
}, TaskContinuationOptions.OnlyOnRanToCompletion).ContinueWith(t => {
    // 前 task の続き(つまり、1st task 成功時のみ)
    Console.WriteLine("3rd task");
    /* 略 */
    return t.Result;
});

上記は、1st task 成功時のみ 2nd task → 3rd task の順で実行される。
1st task で例外が起こった場合、以降の Task は実行されず、
例外も未処理になってしまうため、task が GC される際にファイナライザで例外が発生する。(Webアプリとかで発生するとアプリが死ぬ奴)
※Task のエラー未処理対策は TaskScheduler.UnobservedTaskException

正しい方法としては、最初の Task を一度変数に格納して、そこから成功時・失敗時で別の Task に分離させていく。
→ 変数が増えてコードが複雑になる

では、ContinueWith 内で Task.IsFaulted なら Task.Exception を投げればいいかも?となるかもしれないが、ContinueWith 内に毎回同じ処理を書くのは面倒。
というわけで、次の拡張メソッドを作成。

解決案


/// 
/// task 成功時のみ completionFunc を実行する。
/// エラー・キャンセル情報は、次の Task に渡される。
/// beforeHandler が指定された(null でない)場合、task の Status に関わらず、最初に実行される。
/// 
public static Task<TOut> OnSuccess<TIn, TOut>(
    this Task<TIn> task, Func<TIn, TOut> completionFunc, Action<Task<TIn>> beforeHandler)
{
    return task.ContinueWith(t => t.OnSuccessImpl(completionFunc, beforeHandler))
        .Unwrap();
}

/// 
/// OnSuccess の実装。
/// Task 内で OnSuccess を使う場合は、こちらを使用すると Task ネストが深くなりにくい。
/// 
public static Task<TOut> OnSuccessImpl<TIn, TOut>(
    this Task<TIn> task, Func<TIn, TOut> completionFunc, Action<Task<TIn>> beforeHandler)
{
    if( beforeHandler != null ) beforeHandler(task);
    var tcs = new TaskCompletionSource<TOut>();
    switch( task.Status ){
    case TaskStatus.Canceled:
        tcs.TrySetCanceled();
        break;
    case TaskStatus.Faulted:
        tcs.TrySetException(task.Exception.InnerExceptions);
        break;
    case TaskStatus.RanToCompletion:
        TOut result;
        try {
            result = completionFunc(task.Result);
        } catch( Exception ex ){
            tcs.TrySetException(ex);
            break;
        }
        tcs.TrySetResult(result);
        break;
    }
    return tcs.Task;
}
[使用例]

var task = Task.Factory.StartNew(() => {
    Console.WriteLine("1st task");
    var len = ((string)null).Length; //例外発生
    return 0;
}).OnSuccess(t => {
    // 1st task 成功時のみ
    Console.WriteLine("success task");
    return 0;
}, null).ContinueWith(t => {
    // 必ず実行
    Console.WriteLine("error handling");
    return t.Result;
});
Console.WriteLine(task.Result);

//[結果]
// 1st task
// error handling
//
// 1st task の例外メッセージ

また、このメソッドはエラーを次の Task に流すため、次の効果がある。
  • エラー未処理バグが減る
  • AggregateException が深くならない

0 件のコメント:

コメントを投稿