3

私は現在、ライブラリ API の非同期部分をTasks で適切に公開する方法を学んでいます。これにより、顧客がより簡単に、より使いやすくなります。私は、スレッドプールでスケジュールされていないa をラップするアプローチTaskCompletionSourceを採用することにしました(ここのインスタンスでは、基本的に単なるタイマーであるため、とにかく不要です)。Taskこれは問題なく機能しますが、キャンセルは今のところ少し頭痛の種です.

この例は、トークンにデリゲートを登録する基本的な使用法を示していますが、私の場合よりも少し複雑ですTaskCanceledExceptionドキュメントには、単に戻ってタスクのステータスを に切り替えるか、 (タスクの結果が になる) をRanToCompletionスローするだけで問題ないと書かれています。ただし、例は、に渡されたデリゲートを介して開始されるタスクにのみ関連するか、少なくとも言及しているようです。OperationCanceledExceptionCanceledTaskFactory.StartNew

私のコードは現在(おおよそ)次のとおりです。

public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
  var tcs = new TaskCompletionSource<object>();

  // Regular finish handler
  EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
  // Cancellation
  token.Register(() => {
    tcs.TrySetCanceled();
    CancelAndCleanupFoo(foo);
  });

  RunFoo(foo, callback);
  return tcs.Task;
}

(実行中に結果はなく、例外の可能性もありません。ライブラリのより複雑な場所ではなく、ここから開始することを選択した理由の 1 つです。)

現在のフォームでは、 を呼び出すとTrySetCanceled、タスクが返されるのを待っている場合TaskCompletionSourceは常に が返されます。私の推測では、これは通常の動作であり (そうであることを願っています)、キャンセルを使用する場合は呼び出しを/で囲む必要があります。TaskCanceledException trycatch

を使用しない場合TrySetCanceled、最終的には終了コールバックで実行され、タスクは正常に終了したように見えます。しかし、ユーザーが正常に終了したタスクとキャンセルされたタスクを区別したい場合は、それTaskCanceledExceptionを保証することの副作用ですよね?

私がよく理解していなかったもう 1 つの点:ドキュメントによると、キャンセルに関係するものであっても例外はすべてAggregateExceptionTPL によってラップされます。ただし、私のテストでは、TaskCanceledExceptionラッパーなしで常に直接取得します。ここに何かが欠けていますか、それとも文書化が不十分なだけですか?


TL;DR:

  • タスクが状態に移行するには、Canceled常にそれに対応する例外が必要であり、ユーザーはそれを検出できるように非同期呼び出しをtry/で囲む必要があります。catch
  • ラップTaskCanceledExceptionされていない状態で投げられることも予想され、正常であり、私はここで何も悪いことをしていませんか?
4

3 に答える 3

5

マネージ スレッドのキャンセルに関するドキュメントを読むことを常にお勧めします。完全ではありません。ほとんどの MSDN ドキュメントと同様に、何をすべきかではなく、何ができるかを示します。しかし、キャンセルに関するドットネットのドキュメントよりも明確です。

例は基本的な使用法を示しています

まず、コード例のキャンセルはタスクをキャンセルするだけであり、基になる操作をキャンセルしないことに注意することが重要です。これを行わないことを強くお勧めします。

操作をキャンセルしたい場合は、取得するように更新する必要がありますRunFoo(CancellationToken使用方法については以下を参照してください)。

public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
  var tcs = new TaskCompletionSource<object>();

  // Regular finish handler
  EventHandler<AsyncCompletedEventArgs> callback = (sender, args) =>
  {
    if (args.Cancelled)
    {
      tcs.TrySetCanceled(token);
      CleanupFoo(foo);
    }
    else
      tcs.TrySetResult(null);
  };

  RunFoo(foo, token, callback);
  return tcs.Task;
}

キャンセルできない場合fooは、API がキャンセルをまったくサポートしていません。

public Task Run(IFoo foo) {
  var tcs = new TaskCompletionSource<object>();

  // Regular finish handler
  EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);

  RunFoo(foo, callback);
  return tcs.Task;
}

これは、このシナリオに適したコード手法です (キャンセルされるのは待機あり、タスクによって表される操作ではないため) 「キャンセル可能な待機」の実行は、私の AsyncEx.Tasks ライブラリを介して実行できます。または、独自の同等の拡張メソッドを作成することもできます。

ドキュメントには、単に戻ってタスク ステータスを RanToCompletion に切り替えるか、OperationCanceledException (タスクの結果が Canceled になる) をスローするだけで問題ないと書かれています。

ええ、それらのドキュメントは誤解を招くものです。まず、ただ戻らないでください。実際には操作が正常に完了しなかった場合でも、メソッドはタスクを正常に完了し、操作が正常に完了したことを示します。これは一部のコードでは機能する可能性がありますが、一般的には良い考えではありません。

通常、a に応答する適切な方法CancellationTokenは次のいずれかです。

  • を定期的に呼び出しますThrowIfCancellationRequested。このオプションは、CPU バウンドのコードに適しています。
  • 経由でキャンセル コールバックを登録しますRegister。このオプションは、I/O バウンドのコードに適しています。登録は破棄する必要があることに注意してください。

あなたの特定のケースでは、異常な状況があります。あなたの場合、私は3番目のアプローチを取ります:

  • 「フレームごとの作業」で、チェックしtoken.IsCancellationRequestedます。要求された場合は、AsyncCompletedEventArgs.Cancelledset toでコールバック イベントを発生させtrueます。

これは、最初の適切な方法 (定期的に を呼び出す) と論理的に同等でThrowIfCancellationRequestedあり、例外をキャッチし、それをイベント通知に変換します。例外なく。

タスクが返されるのを待っていると、常に TaskCanceledException が発生します。私の推測では、これは通常の動作であり (そうであることを願っています)、キャンセルを使用する場合は、呼び出しの周りに try/catch をラップすることが期待されます。

キャンセル可能なタスクの適切な消費awaitコードは、 try/catchおよび catchOperationCanceledExceptionでラップすることです。さまざまな理由 (多くの歴史的理由) により、一部の API が原因OperationCanceledExceptionとなり、一部の API が原因となりTaskCanceledExceptionます。からTaskCanceledException派生しているためOperationCanceledException、消費するコードは、より一般的な例外をキャッチできます。

でも、ユーザーが正常に終了したタスクとキャンセルされたタスクを区別したい場合、[キャンセル例外] はそれを保証するための副作用ですよね?

はい、それは受け入れられたパターンです。

ドキュメントによると、キャンセルに関連する例外であっても、TPL によって AggregateException にラップされることが示唆されています。

これは、コードがタスクで同期的にブロックする場合にのみ当てはまります。そもそもこれを避けるべきです。したがって、ドキュメントは間違いなく誤解を招くものです。

ただし、私のテストでは、ラッパーなしで、常に TaskCanceledException を直接取得していました。

awaitAggregateExceptionラッパーを回避します。

コメント説明のための更新CleanupFoo キャンセル方法です。

CancellationToken最初に、によって開始されたコード内で直接使用することをお勧めしRunFooます。そのアプローチはほぼ確実に簡単になります。

ただし、やむを得ずキャンセルする場合はCleanupFoo、キャンセルが必要ですRegister。その登録を破棄する必要があります。これを行う最も簡単な方法は、実際には 2 つの異なる方法に分割することです。

private Task DoRun(IFoo foo) {
  var tcs = new TaskCompletionSource<object>();

  // Regular finish handler
  EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);

  RunFoo(foo, callback);
  return tcs.Task;
}

public async Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
  var tcs = new TaskCompletionSource<object>();
  using (token.Register(() =>
      {
        tcs.TrySetCanceled(token);
        CleanupFoo();
      });
  {
    var task = DoRun(foo);
    try
    {
      await task;
      tcs.TrySetResult(null);
    }
    catch (Exception ex)
    {
      tcs.TrySetException(ex);
    }
  }
  await tcs.Task;
}

リソースのリークを防ぎながら、結果を適切に調整して伝播することは、非常に厄介です。コードをCancellationToken直接使用できる場合は、はるかにクリーンになります。

于 2017-04-28T13:55:58.193 に答える