web-dev-qa-db-ja.com

CancellationTokenSourceを破棄するタイミング

クラスCancellationTokenSourceは使い捨てです。 Reflectorをざっと見てみると、(おそらく)非管理対象リソースであるKernelEventの使用が証明されています。 CancellationTokenSourceにはファイナライザがないため、破棄しない場合、GCは処理しません。

一方、MSDNの記事 Managed Threadsでのキャンセル にリストされているサンプルを見ると、1つのコードスニペットのみがトークンを破棄しています。

コードでそれを処分する適切な方法は何ですか?

  1. 並列タスクを開始するコードは、待たなければusingでラップできません。そして、待機しない場合にのみキャンセルするのは理にかなっています。
  2. もちろん、ContinueWith呼び出しを使用してタスクにDisposeを追加できますが、その方法はありますか?
  3. キャンセル可能なPLINQクエリはどうですか?これらのクエリは、同期を取り戻さず、最後に何かを行うだけですか? .ForAll(x => Console.Write(x))としましょうか?
  4. 再利用可能ですか?同じトークンを複数の呼び出しに使用して、ホストコンポーネントと一緒に破棄できますか?

ResetおよびIsCancelRequestedフィールドをクリーンアップするTokenメソッドのようなものがないため、タスクを開始するたびに(または、 PLINQクエリ)新しいクエリを作成する必要があります。本当ですか?はいの場合、私の質問は、これらの多くのDisposeインスタンスでCancellationTokenSourceを処理するための正しい推奨される戦略は何ですか?

139

CancellationTokenSourceでDisposeを呼び出す必要があるかどうかを話すと、プロジェクトでメモリリークが発生し、CancellationTokenSourceが問題であることがわかりました。

私のプロジェクトには、データベースを常に読み取り、さまざまなタスクを実行するサービスがあります。また、リンクされたキャンセルトークンをワーカーに渡していたため、データの処理が完了した後でもキャンセルトークンが破棄されず、メモリリークが発生しました。

MSDN マネージスレッドのキャンセル 明確に述べています:

終了したら、リンクされたトークンソースでDisposeを呼び出す必要があることに注意してください。より完全な例については、「 方法:複数のキャンセルリクエストをリッスンする 」を参照してください。

私の実装ではContinueWithを使用しました。

64
Gruzilkin

現在の答えはどれも満足できるものではないと思いました。調査した後、Stephen Toubからのこの返信を見つけました( reference ):

場合によります。 .NET 4では、CTS.Disposeは2つの主な目的を果たしました。 CancellationTokenのWaitHandleがアクセスされた場合(したがって、遅延的に割り当てられた場合)、Disposeはそのハンドルを破棄します。さらに、CTSがCreateLinkedTokenSourceメソッドを介して作成された場合、DisposeはリンクされたトークンからCTSのリンクを解除します。 .NET 4.5では、Disposeには追加の目的があります。CTSがカバーの下でタイマーを使用する場合(CancelAfterが呼び出された場合など)、タイマーは破棄されます。

CancellationToken.WaitHandleが使用されることは非常にまれであるため、通常、その後のクリーンアップはDisposeを使用する大きな理由にはなりません。 ただし、CreateLinkedTokenSourceを使用してCTSを作成している場合、またはCTSのタイマー機能を使用している場合は、Disposeを使用する方がインパクトがあります。

私が思う大胆な部分は重要な部分です。彼は「より影響力の強い」を使用しているため、少しあいまいです。これらの状況でDisposeを呼び出すことを意味すると解釈していますが、そうでない場合はDisposeを使用する必要はありません。

35
Jesse Good

ILSpyでCancellationTokenSourceを確認しましたが、実際にはManualResetEventであるm_KernelEventしか見つかりません。これはWaitHandleオブジェクトのラッパークラスです。これはGCによって適切に処理される必要があります。

25
Bryan Crosby

常にCancellationTokenSourceを破棄する必要があります。

廃棄方法は、シナリオによって異なります。いくつかの異なるシナリオを提案します。

  1. usingは、待機中の並列作業でCancellationTokenSourceを使用している場合にのみ機能します。それがあなたのセナリオなら、それは素晴らしい、それは最も簡単な方法です。

  2. タスクを使用するときは、ContinueWithを破棄するように指定したCancellationTokenSourceタスクを使用します。

  3. Plinqでは、usingを使用できます。これは、並列実行しているが、並列実行中のすべてのワーカーが終了するのを待っているためです。

  4. UIの場合、単一のキャンセルトリガーに関連付けられていないキャンセル可能な操作ごとに新しいCancellationTokenSourceを作成できます。 List<IDisposable>そして、各ソースをリストに追加し、コンポーネントが破棄されるときにそれらすべてを破棄します。

  5. スレッドの場合、すべてのワーカースレッドを結合し、すべてのワーカースレッドが終了したら単一のソースを閉じる新しいスレッドを作成します。 CancellationTokenSource、いつ廃棄するか? を参照してください。

常に方法があります。 IDisposableインスタンスは常に破棄する必要があります。サンプルは、コアの使用法を示すための簡単なサンプルであるため、またはサンプルのデモを行うクラスのすべての側面を追加するのが非常に複雑であるため、しばしばそうではありません。サンプルは単なるサンプルであり、必ずしも(または通常でも)生産品質のコードではありません。すべてのサンプルがそのまま製品コードにコピーできるわけではありません。

20
Samuel Neff

この答えはまだGoogle検索で出てきており、投票された答えは完全なストーリーを提供するものではないと思います。 ソースコード for CancellationTokenSource(CTS)およびCancellationToken(CT)を見た後、ほとんどのユースケースで次のコードシーケンスが適切であると考えています。

if (cancelTokenSource != null)
{
    cancelTokenSource.Cancel();
    cancelTokenSource.Dispose();
    cancelTokenSource = null;
}

m_kernelHandle上記の内部フィールドは、CTSクラスとCTクラスの両方でWaitHandleプロパティをサポートする同期オブジェクトです。そのプロパティにアクセスした場合にのみインスタンス化されます。したがって、WaitHandleを使用してTaskを使用している場合を除き、Disposeを呼び出してdisposeを呼び出しても効果はありません。

もちろん、areを使用している場合、上記の他の回答で提案されていることを実行し、WaitHandleの呼び出しを_(VARIABLE WaitHandleのWindows APIドキュメント で説明されているように、結果は未定義であるため、ハンドルを使用した操作は完了します。

14
jlyonsmith

私がこれを尋ねて多くの有益な答えを得てから長い時間が経ちましたが、これに関連する興味深い問題に遭遇し、別の種類の答えとしてここに投稿しようと思いました:

CancellationTokenSource.Dispose()は、CTSのTokenプロパティを取得しようとする人がいないことが確実な場合にのみ呼び出す必要があります。それ以外の場合は、レースであるため、notと呼ぶ必要があります。たとえば、こちらをご覧ください。

https://github.com/aspnet/AspNetKatana/issues/108

この問題の修正では、以前にcts.Cancel(); cts.Dispose();を実行していたコードが、キャンセル状態を監視するためにキャンセルトークンを取得しようとするほど運が悪かったためにcts.Cancel();を実行するように編集されましたafterDisposeが呼び出された場合、残念ながらObjectDisposedExceptionに加えて、計画していたOperationCanceledExceptionも処理する必要があります。 。

この修正に関連するもう1つの重要な観察は、Tratcherによるものです。「破棄は、キャンセルがすべて同じクリーンアップを行うため、キャンセルされないトークンに対してのみ必要です。」つまり、破棄する代わりにCancel()するだけで十分です!

3