6

特定のタスクまたはデリゲートの背後にあるソースを確認せずに、タスク/デリゲートの「制御されていない」キャンセルと制御/協調を区別できないことがわかりました。

OperationCanceledException具体的には、「下位レベルの操作」からスローされたものをキャッチするときに、参照されたトークンが現在の操作のトークンと一致しない場合、それは失敗/エラーとして解釈されるべきであると常に想定してきました。これは、あきらめた(終了した)という「下位レベルの操作」からの声明ですが、あなたがそうするように頼んだからではありません

残念ながら、キャンセルの理由としてTaskCompletionSourcea を関連付けることはできません。CancellationTokenそのため、組み込みのスケジューラによってサポートされていないタスクは、キャンセルの理由を伝えることができず、協調的なキャンセルをエラーとして誤って報告する可能性があります。

更新: .NET 4.6 の時点で、TaskCompletionSourceは、またはの新しいオーバーロードが使用されている場合に関連付けることができます。CancellationToken SetCanceledTrySetCanceled

たとえば、次の

public Task ShouldHaveBeenAsynchronous(Action userDelegate, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<object>();

    try
    {
      userDelegate();
      tcs.SetResult(null);   // Indicate completion
    }
    catch (OperationCanceledException ex)
    {
      if (ex.CancellationToken == ct)
        tcs.SetCanceled(); // Need to pass ct here, but can't
      else
        tcs.SetException(ex);
    }
    catch (Exception ex)
    {
      tcs.SetException(ex);
    }

    return tcs.Task;
}

private void OtherSide()
{
    var cts = new CancellationTokenSource();
    var ct = cts.Token;
    cts.Cancel();
    Task wrappedOperation = ShouldHaveBeenAsynchronous(
        () => { ct.ThrowIfCancellationRequested(); }, ct);

    try
    {
        wrappedOperation.Wait();
    }
    catch (AggregateException aex)
    {
        foreach (var ex in aex.InnerExceptions
                              .OfType<OperationCanceledException>())
        {
            if (ex.CancellationToken == ct)
                Console.WriteLine("OK: Normal Cancellation");
            else
                Console.WriteLine("ERROR: Unexpected cancellation");
        }
    }
}

すべてのコンポーネントに配布されたキャンセル トークンを介してキャンセルが要求された場合でも、"エラー: 予期しないキャンセル" が発生します。

主な問題は、TaskCompletionSource が CancellationToken を認識していないことですが、非同期操作を Tasks でラップするための「go to」メカニズムがこれを追跡できない場合、インターフェイス全体で追跡されることを期待できないと思います(ライブラリ) 境界。

実際、TaskCompletionSource はこれを処理できますが、必要な TrySetCanceled オーバーロードは内部であるため、mscorlib コンポーネントのみが使用できます。

キャンセルがタスクとデリゲートの境界を越えて「処理」されたことを伝えるパターンを持っている人はいますか?

4

3 に答える 3

2

タスク/デリゲートの実装方法の詳細を確認せずに、制御されたものと「制御されていない」キャンセルを区別できないことがわかりました。

さらに、タスクの待機中または待機中に例外をキャッチしたという事実は、OperationCanceledException必ずしもタスクStatusTaskStatus.Canceled. そうかもしれませんTaskStatus.Faulted

あなたが求めているものを実装するには、おそらくいくつかのオプションがあります。私はそれを使用してそれをContinueWith行い、その継続タスクを元のタスクではなくクライアントコードに渡しますTaskCompletionSource.Task:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    public static class TaskExt
    {
        public static Task<TResult> TaskWithCancellation<TResult>(
            this TaskCompletionSource<TResult> @this,
            CancellationToken token)
        {
            var registration = token.Register(() => @this.TrySetCanceled());
            return @this.Task.ContinueWith(
                task => { registration.Dispose(); return task.Result; },
                token, 
                TaskContinuationOptions.LazyCancellation | 
                    TaskContinuationOptions.ExecuteSynchronously, 
                TaskScheduler.Default);
        }
    }

    class Program
    {
        static async Task OtherSideAsync(Task task, CancellationToken token)
        {
            try
            {
                await task;
            }
            catch (OperationCanceledException ex)
            {
                if (token != ex.CancellationToken)
                    throw;
                Console.WriteLine("Cancelled with the correct token");
            }
        }

        static void Main(string[] args)
        {
            var cts = new CancellationTokenSource(1000); // cancel in 1s
            var tcs = new TaskCompletionSource<object>();

            var taskWithCancellation = tcs.TaskWithCancellation(cts.Token);
            try
            {
                OtherSideAsync(taskWithCancellation, cts.Token).Wait();
            }
            catch (AggregateException ex)
            {
                Console.WriteLine(ex.InnerException.Message);
            }
            Console.ReadLine();
        }
    }
}

の使用に注意してください。TaskContinuationOptions.LazyCancellationこれは、継続タスクがタスクの前に完了しないようにするためのものですtcs.Task(キャンセルが を介して要求された場合token)。

tcs.TrySetCanceledまた、を介してキャンセルが要求される前に が呼び出された場合token、結果のタスクはキャンセルされた状態ではなく、失敗した状態になることにも注意してください(taskWithCancellation.IsFaulted == trueただしtaskWithCancellation.IsCancelled == false)。token暗黙的なキャンセルと明示的なキャンセルの両方でキャンセル ステータスを伝達する場合は、拡張子を次のようtcs.TrySetCanceledに変更します。TaskWithCancellation

public static Task<TResult> TaskWithCancellation<TResult>(
    this TaskCompletionSource<TResult> @this,
    CancellationToken token)
{
    var registration = token.Register(() => @this.TrySetCanceled());
    return @this.Task.ContinueWith(
        task => { registration.Dispose(); return task; },
        token,
        TaskContinuationOptions.LazyCancellation | 
            TaskContinuationOptions.ExecuteSynchronously,
        TaskScheduler.Default).Unwrap();
}

コメントに対処するために更新されました:

ベースのライブラリ APIの典型的な設計ではTask、クライアント コードがキャンセル トークンを API に提供し、API は提供されたトークンに関連付けられTaskたを返します。その後、API のクライアント コードは、キャンセル例外をキャッチするときにトークン マッチングを実行できます。

の正確な目的は、そのTaskWithCancellationようなものを作成Taskしてクライアントに返すことです。オリジナルTaskCompletionSource.Taskがクライアントに公開されることはありません。トークンが に渡されたためにキャンセルが発生ContinueWithし、継続タスクに関連付けられます。OTOH、、token.RegisterおよびTrySetCanceledTaskContinuationOptions.LazyCancellation、登録のクリーンアップを含め、物事が正しい順序で行われるようにするためだけに使用されます。

于 2014-05-15T05:29:20.660 に答える
2

記録のために: これは .NET Framework 4.6 以降で修正されています。TaskCompletionSource.TrySetCanceled メソッド (CancellationToken)

于 2015-09-06T10:08:00.797 に答える