196

クラスCancellationTokenSourceは使い捨てです。Reflectorをざっと見てみるとKernelEvent、(非常に可能性が高い)管理されていないリソースの使用法がわかります。CancellationTokenSourceファイナライザーがないので、処分しないとGCは行いません。

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

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

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

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

4

7 に答える 7

98

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

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

管理対象スレッドでのMSDNキャンセルは、次のように明確に述べています。

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

ContinueWithは自分の実装で使用しました。

于 2012-09-18T10:05:07.470 に答える
60

現在の答えはどれも満足のいくものではないと思いました。調査した後、私は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する必要はありません。

于 2015-06-15T21:45:14.643 に答える
31

常に処分する必要がありますCancellationTokenSource

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

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

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

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

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

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

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

于 2013-08-26T13:50:56.653 に答える
28

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

于 2011-08-05T18:54:08.997 に答える
25

この答えはまだグーグル検索で出てきており、投票された答えは完全な話を与えるものではないと私は信じています。(CTS)と(CT)のソースコードを調べた後、ほとんどのユースケースでは次のコードシーケンスで問題ないと思います。CancellationTokenSourceCancellationToken

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

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

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

于 2014-07-06T10:28:58.437 に答える
23

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

CancellationTokenSource.Dispose()誰もCTSの資産を取得しようとしないことが確実な場合にのみ、電話をかける必要がありTokenます。それ以外の場合は、競合状態を引き起こすため、を呼び出さないでください。Dispose()たとえば、ここを参照してください。

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

この問題の修正では、以前に行ったコードcts.Cancel(); cts.Dispose();が編集されました。これは、呼び出された後cts.Cancel();にキャンセル状態を監視するためにキャンセルトークンを取得しようとする不運な人が、残念ながら処理する必要があるためです。彼らが計画していたこと。 DisposeObjectDisposedExceptionOperationCanceledException

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

于 2018-07-07T05:10:31.443 に答える
7

CancellationTokenSourceをにバインドし、関連付けが完了したときにが破棄されることTaskを保証するスレッドセーフクラスを作成しました。ロックを使用して、廃棄中または廃棄後にキャンセルされないようにします。これは、次のように記載されているドキュメントに準拠するために発生します。CancellationTokenSourceTaskCancellationTokenSource

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

そしてまた

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

クラスは次のとおりです。

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

    // Represents a cancelable operation that signals its completion when disposed
    private class Operation : IDisposable
    {
        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 (this) if (!_disposed) _cts.Cancel(); }

        void IDisposable.Dispose() // It is disposed once and only once
        {
            try { lock (this) { _cts.Dispose(); _disposed = true; } }
            finally { _completionSource.SetResult(true); }
        }
    }

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

    public bool IsRunning => Volatile.Read(ref _activeOperation) != null;

    public async Task<TResult> RunAsync<TResult>(
        Func<CancellationToken, Task<TResult>> action,
        CancellationToken extraToken = default)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));
        var cts = CancellationTokenSource.CreateLinkedTokenSource(extraToken);
        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
                    // The Completion never fails
                }
                cts.Token.ThrowIfCancellationRequested();
                var task = action(cts.Token); // Invoke on 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);
            }
        }
        // The cts is disposed along with the operation
    }

    public Task RunAsync(Func<CancellationToken, Task> action,
        CancellationToken extraToken = default)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));
        return RunAsync<object>(async ct =>
        {
            await action(ct).ConfigureAwait(false);
            return null;
        }, extraToken);
    }

    public Task CancelAsync()
    {
        var operation = Volatile.Read(ref _activeOperation);
        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いる追加の引数を受け入れます。このオプションのトークンを提供すると、高度なシナリオで役立つ場合があります。CancellationTokenCancellationTokenSource

于 2020-05-08T14:54:55.030 に答える