いつCancellationTokenSourceを破棄するのですか?


163

クラスCancellationTokenSourceは使い捨てです。Reflectorをざっと見てみるKernelEventと、(ほとんどの場合)管理されていないリソースの使用がわかります。にCancellationTokenSourceはファイナライザがないため、破棄しない場合、GCはそれを行いません。

一方、MSDNの記事「マネージスレッドでのキャンセル」に記載されているサンプルを見ると、トークンを破棄するコードスニペットは1つだけです。

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

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

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

回答:


81

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

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

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

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

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


14
これは、Bryan Crosbyが現在認めている回答では重要な省略です。リンクされた CTS を作成する、メモリリークのリスクがあります。このシナリオは、登録解除されないイベントハンドラーに非常に似ています。
セーレンBoisen

5
同じ問題が原因でリークが発生しました。プロファイラーを使用して、リンクされたCTSインスタンスへの参照を保持するコールバック登録を確認できました。ここで CTS Dispose実装のコードを調べることは非常に洞察に富んでおり、@SørenBoisenとイベントハンドラー登録リークの比較を強調しています。
BitMask777 2016

上記のコメントは、@ Bryan Crosbyによる他の回答が受け入れられたときの議論の状態を反映しています。
George Mamaladze 2016年

2020年のドキュメントでは、次のように明確に述べられています。Important: The CancellationTokenSource class implements the IDisposable interface. You should be sure to call the CancellationTokenSource.Dispose method when you have finished using the cancellation token source to free any unmanaged resources it holds.- docs.microsoft.com
en

44

現在の答えはどれも満足できるとは思いませんでした。調べたところ、Stephen Toub(参考文献)からのこの返信が見つかりました。

場合によります。.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必要はありません。


10
より影響が大きいということは、子CTSが親CTSに追加されることを意味します。あなたが子供を処分しないと、親が長生きしている場合、リークがあります。したがって、リンクされたものを処分することが重要です。
グリゴリー

26

私はILSpyでを調べましたCancellationTokenSourceが、m_KernelEvent実際にはManualResetEventWaitHandleオブジェクトのラッパークラスであるだけを見つけることができます。これは、GCによって適切に処理されます。


7
GCがすべてをクリーンアップするのと同じ気持ちです。確認してみます。この場合、Microsoftが実装したdisposeはなぜですか?イベントコールバックを取り除き、おそらく第2世代GCへの伝播を回避するため。この場合、Disposeの呼び出しはオプションです-可能であれば呼び出し、無視するだけではありません。私が思う最善の方法ではありません。
George Mamaladze、2011

4
この問題を調査しました。CancellationTokenSourceはガベージコレクションを取得します。GEN 1 GCでそれを行うには、disposeを支援することができます。受け入れた。
George Mamaladze

1
私はこれと同じ調査を個別に行い、同じ結論に達しました。簡単にできる場合は破棄しますが、CancellationTokenを送信したまれではないが前代未聞のケースでは、そうしようとしないでください。お仕置きが終わったら、ポストカードを書き終えて、それが終わったことを知らせるのを待ちたくない。これは、CancellationTokenが何のために使用されるかという性質上、時々起こります。そしてそれは本当に大丈夫だと私は約束します。
Joe Amenta

6
上記のコメントはリンクされたトークンソースには適用されません。私はこれらを処分しないままにしても大丈夫であることを証明できませんでした。このスレッドとMSDNの知恵はそうではないかもしれないと示唆しています。
Joe Amenta

23

常に処分してくださいCancellationTokenSource

それを処分する方法は、シナリオに完全に依存します。いくつかの異なるシナリオを提案します。

  1. usingCancellationTokenSourceあなたが待っているいくつかの並行作業で使用しているときにのみ機能します。それがあなたのシナリオであるならば、それは素晴らしい、それは最も簡単な方法です。

  2. タスクを使用するときは、ContinueWith指定したとおりにタスクを使用してを破棄してくださいCancellationTokenSource

  3. plinqの場合usingは、並列で実行しているが、並列実行中のすべてのワーカーが完了するのを待機しているため、使用できます。

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

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

常に方法があります。 IDisposableインスタンスは常に破棄する必要があります。サンプルは、コアの使用法を示す簡単なサンプルであるか、またはデモされているクラスのすべての側面を追加することがサンプルにとって過度に複雑になるため、多くの場合そうではありません。サンプルは単なるサンプルであり、必ずしも(または通常は)製品品質のコードではありません。すべてのサンプルをそのままプロダクションコードにコピーできるわけではありません。


ポイント2でawait、タスクで使用できず、待機後に来るコードでCancellationTokenSourceを破棄できない理由はありますか?
stijn

14
注意点があります。await操作中にCTSがキャンセルされた場合、が原因で再開することがありOperationCanceledExceptionます。次にを呼び出しますDispose()。ただし、まだ実行中で対応するを使用している操作がある場合、ソースが破棄されていてもCancellationToken、そのトークンは引き続き報告されます。キャンセルコールバックを登録しようとすると、BOOM!、。操作が正常に完了した後に呼び出すのに十分安全です。実際に何かをキャンセルする必要がある場合、それは本当にトリッキーになります。CanBeCanceledtrueObjectDisposedExceptionDispose()
マイクストロベル2014年

8
Mike Strobelによって与えられた理由で反対投票-常にDisposeを呼び出すようにルールを強制すると、非同期の性質のため、CTSとTaskを処理するときに困難な状況に陥る可能性があります。ルールは代わりに:常にリンクされたトークンソースを破棄します。
セーレンBoisen

1
あなたのリンクは削除された回答に行きます。
Trisped

19

この答えはGoogle検索でまだ出てきており、投票された答えは完全な物語を提供していないと思います。(CTS)と(CT)のソースコードを調べた後、ほとんどのユースケースで次のコードシーケンスが適切であると思います。CancellationTokenSourceCancellationToken

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

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

もちろん、あなたがいる場合しているあなたが遅延呼び出し上記の他の回答で提案されて何をすべき、それを使ってDispose任意のまで、WaitHandleハンドルを使用して操作が完了するように記述されている、ので、WaitHandleのWindows APIドキュメント、結果は不定です。


7
MSDNの記事「Managed Threadsでのキャンセル」には、「リスナーはIsCancellationRequestedポーリング、コールバック、または待機ハンドルによってトークンのプロパティの値を監視します」と記載されています。言い換えると、待機ハンドルを使用するのはあなた(つまり、非同期要求を行う人)ではなく、リスナー(つまり、要求に応答する人)かもしれません。つまり、破棄を担当するユーザーは、待機ハンドルを使用するかどうかを効果的に制御できません。
herzbube

MSDNによると、例外のある登録済みコールバックは.Cancelをスローします。この場合、コードは.Dispose()を呼び出しません。コールバックはこれを行わないように注意する必要がありますが、発生する可能性があります。
Joseph Lennox

11

私がこれを質問してから多くの役立つ回答を得たのは久しぶりですが、これに関連する興味深い問題に遭遇し、別の種類の回答としてここに投稿すると思いました:

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

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

この問題に対する修正、以前になかったコードでcts.Cancel(); cts.Dispose();だけ行うに編集されたcts.Cancel();誰もキャンセルがその取り消し状態を観察するためにトークンを取得しようとすると不運なのでので、後に Dispose、残念ながらもハンドルにする必要がありますと呼ばれているObjectDisposedExceptionに加えて- OperationCanceledException彼らが計画していたこと。

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


1

私は結合することをスレッドセーフなクラスを作りCancellationTokenSourceTask、そしてことを保証しCancellationTokenSource、その関連に配置されますTask完了します。ロックを使用して、CancellationTokenSource廃棄中または廃棄後にキャンセルされないようにします。これは、次のドキュメントに準拠するために発生します。

このDisposeメソッドは、CancellationTokenSourceオブジェクトに対する他のすべての操作が完了したときにのみ使用する必要があります。

そしてまた

このDisposeメソッドは、CancellationTokenSourceを使用できない状態のままにします。

ここにクラスがあります:

public class CancelableExecution
{
    private readonly bool _allowConcurrency;
    private Operation _activeOperation;

    private class Operation : IDisposable
    {
        private readonly object _locker = new object();
        private readonly CancellationTokenSource _cts;
        private readonly TaskCompletionSource<bool> _completionSource;
        private bool _disposed;

        public Task Completion => _completionSource.Task; // Never fails

        public Operation(CancellationTokenSource cts)
        {
            _cts = cts;
            _completionSource = new TaskCompletionSource<bool>(
                TaskCreationOptions.RunContinuationsAsynchronously);
        }
        public void Cancel()
        {
            lock (_locker) if (!_disposed) _cts.Cancel();
        }
        void IDisposable.Dispose() // Is called only once
        {
            try
            {
                lock (_locker) { _cts.Dispose(); _disposed = true; }
            }
            finally { _completionSource.SetResult(true); }
        }
    }

    public CancelableExecution(bool allowConcurrency)
    {
        _allowConcurrency = allowConcurrency;
    }
    public CancelableExecution() : this(false) { }

    public bool IsRunning =>
        Interlocked.CompareExchange(ref _activeOperation, null, null) != null;

    public async Task<TResult> RunAsync<TResult>(
        Func<CancellationToken, Task<TResult>> taskFactory,
        CancellationToken extraToken = default)
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource(extraToken, default);
        using (var operation = new Operation(cts))
        {
            // Set this as the active operation
            var oldOperation = Interlocked.Exchange(ref _activeOperation, operation);
            try
            {
                if (oldOperation != null && !_allowConcurrency)
                {
                    oldOperation.Cancel();
                    await oldOperation.Completion; // Continue on captured context
                }
                var task = taskFactory(cts.Token); // Run in the initial context
                return await task.ConfigureAwait(false);
            }
            finally
            {
                // If this is still the active operation, set it back to null
                Interlocked.CompareExchange(ref _activeOperation, null, operation);
            }
        }
    }

    public Task RunAsync(Func<CancellationToken, Task> taskFactory,
        CancellationToken extraToken = default)
    {
        return RunAsync<object>(async ct =>
        {
            await taskFactory(ct).ConfigureAwait(false);
            return null;
        }, extraToken);
    }

    public Task CancelAsync()
    {
        var operation = Interlocked.CompareExchange(ref _activeOperation, null, null);
        if (operation == null) return Task.CompletedTask;
        operation.Cancel();
        return operation.Completion;
    }

    public bool Cancel() => CancelAsync() != Task.CompletedTask;
}

CancelableExecutionクラスの主なメソッドはRunAsyncおよびCancelです。デフォルトでは、同時操作は許可されていません。つまり、RunAsync、2回目のは、新しい操作を開始する前に、前の操作(まだ実行中の場合)の完了を通知なしにキャンセルして待機します。

このクラスは、あらゆる種類のアプリケーションで使用できます。ただし、主な用途は、UIアプリケーション、非同期操作を開始およびキャンセルするためのボタンを備えたフォーム内、または選択したアイテムが変更されるたびに操作をキャンセルして再開するリストボックスでの使用です。最初のケースの例を次に示します。

private readonly CancelableExecution _cancelableExecution = new CancelableExecution();

private async void btnExecute_Click(object sender, EventArgs e)
{
    string result;
    try
    {
        Cursor = Cursors.WaitCursor;
        btnExecute.Enabled = false;
        btnCancel.Enabled = true;
        result = await _cancelableExecution.RunAsync(async ct =>
        {
            await Task.Delay(3000, ct); // Simulate some cancelable I/O operation
            return "Hello!";
        });
    }
    catch (OperationCanceledException)
    {
        return;
    }
    finally
    {
        btnExecute.Enabled = true;
        btnCancel.Enabled = false;
        Cursor = Cursors.Default;
    }
    this.Text += result;
}

private void btnCancel_Click(object sender, EventArgs e)
{
    _cancelableExecution.Cancel();
}

このRunAsyncメソッドはCancellationToken、内部で作成されたにリンクされている追加を引数として受け入れますCancellationTokenSource。このオプションのトークンを指定すると、事前のシナリオで役立つ場合があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.