[C#] CancellationTokenSource를 언제 처분해야합니까?

수업 CancellationTokenSource은 일회용입니다. Reflector를 간략히 살펴보면 KernelEvent관리되지 않는 리소스 인 (아마도) 사용이 증명됩니다 . 종료자가 CancellationTokenSource없으므로 처리하지 않으면 GC가 처리하지 않습니다.

반면에 MSDN 기사 Managed Threads에서 Cancellation에 나열된 샘플을 보면 하나의 코드 스 니펫 만 토큰을 처리합니다.

코드로 처리하는 올바른 방법은 무엇입니까?

  1. using기다리지 않으면 병렬 작업을 시작하는 코드를 래핑 할 수 없습니다 . 기다리지 않는 경우에만 취소하는 것이 좋습니다.
  2. 물론 전화로 ContinueWith작업을 추가 할 수 Dispose있지만 그 방법이 있습니까?
  3. 다시 동기화되지 않고 결국 무언가를 수행하는 취소 가능한 PLINQ 쿼리는 어떻습니까? 하자 말 .ForAll(x => Console.Write(x))?
  4. 재사용이 가능합니까? 여러 토큰에 동일한 토큰을 사용한 다음 호스트 구성 요소와 함께 처리 할 수 ​​있습니까?

Reset정리 방법 IsCancelRequestedToken필드 와 같은 방법 이 없기 때문에 재사용 할 수 없다고 가정하므로 작업 (또는 PLINQ 쿼리)을 시작할 때마다 새 것을 만들어야합니다. 사실인가요? 그렇다면 제 질문은 Dispose그러한 많은 CancellationTokenSource인스턴스 를 처리하기위한 정확하고 권장되는 전략은 무엇 입니까?



답변

Dispose on을 호출 해야하는지 여부에 대해 말하면 CancellationTokenSource… 프로젝트에서 메모리 누수 CancellationTokenSource가 발생하여 문제였습니다.

내 프로젝트에는 지속적으로 데이터베이스를 읽고 다른 작업을 수행하는 서비스가 있으며 링크 취소 토큰을 작업자에게 전달하고 있었으므로 데이터 처리가 끝난 후에도 취소 토큰이 폐기되지 않아 메모리 누수가 발생했습니다.

관리되는 스레드의 MSDN 취소는 다음과 같이 명확하게 설명합니다.

Dispose연결이 완료되면 연결된 토큰 소스를 호출해야 합니다. 보다 완전한 예는 방법 : 여러 취소 요청 수신을 참조하십시오 .

ContinueWith구현에 사용 했습니다.


답변

현재 답변 중 어느 것도 만족스럽지 않다고 생각했습니다. 조사한 후 Stephen Toub ( reference )의 답장을 찾았습니다 .

때에 따라 다르지. .NET 4에서 CTS.Dispose는 두 가지 주요 목적을 수행했습니다. CancellationToken의 WaitHandle에 액세스 한 경우 (따라서 지연 할당) 해당 처리가 처리됩니다. 또한 CTS가 CreateLinkedTokenSource 메서드를 통해 생성 된 경우 Dispose는 연결된 토큰에서 CTS를 연결 해제합니다. .NET 4.5에서 Dispose에는 추가 목적이 있습니다. 즉, CTS가 커버 아래에서 타이머를 사용하는 경우 (예 : CancelAfter가 호출 된 경우) 타이머가 삭제됩니다.

CancellationToken.WaitHandle을 사용하는 경우는 매우 드물기 때문에 일반적으로 Dispose를 사용하는 것이 좋습니다.
그러나 CreateLinkedTokenSource를 사용하여 CTS를 만들거나 CTS의 타이머 기능을 사용하는 경우 Dispose를 사용하는 것이 더 영향을 줄 수 있습니다.

내가 생각하는 대담한 부분은 중요한 부분입니다. 그는 “더 충격적인”것을 사용하여 조금 애매하게 만듭니다. Dispose그런 상황에서 전화 를해야한다는 의미로 해석하고 있습니다. 그렇지 않으면 사용 Dispose하지 않아도됩니다.


답변

나는 ILSpy를 살펴 보았지만 실제로 는 객체 의 래퍼 클래스 인 CancellationTokenSource만 찾을 수 있습니다 . 이것은 GC에 의해 올바르게 처리되어야합니다.m_KernelEventManualResetEventWaitHandle


답변

항상 폐기해야합니다 CancellationTokenSource.

폐기 방법은 시나리오에 따라 다릅니다. 몇 가지 다른 시나리오를 제안합니다.

  1. usingCancellationTokenSource당신이 기다리고있는 병렬 작업에 사용할 때만 작동합니다 . 그것이 당신의 상원이라면, 가장 쉬운 방법입니다.

  2. 작업을 사용할 때는 ContinueWith폐기 표시에 따라 작업을 사용하십시오 CancellationTokenSource.

  3. plinq using의 경우 병렬로 실행하지만 모든 병렬 실행 작업자가 완료되기를 기다리는 동안 사용할 수 있습니다 .

  4. UI의 CancellationTokenSource경우 단일 취소 트리거에 연결되지 않은 각 취소 가능한 작업에 대해 새 항목 을 만들 수 있습니다 . a를 유지하고 List<IDisposable>각 소스를 목록에 추가하여 구성 요소를 폐기 할 때 모든 소스를 버립니다.

  5. 스레드의 경우 모든 작업자 스레드를 결합하고 모든 작업자 스레드가 완료되면 단일 소스를 닫는 새 스레드를 작성하십시오. 처분시기는 CancellationTokenSource를 참조하십시오 .

항상 방법이 있습니다. IDisposable인스턴스는 항상 폐기해야합니다. 샘플은 핵심 사용을 보여주기위한 빠른 샘플이거나 시연되는 클래스의 모든 측면을 추가하는 것이 샘플에 대해 지나치게 복잡하기 때문에 종종 샘플이 아닙니다. 샘플은 샘플 일 뿐이며 반드시 프로덕션 품질 코드 일 필요는 없습니다. 모든 샘플을 프로덕션 코드에 그대로 복사 할 수있는 것은 아닙니다.


답변

이 답변은 여전히 ​​Google 검색에 표시되며 투표 결과가 전체 내용을 제공하지는 않는다고 생각합니다. (CTS) 및 (CT) 의 소스 코드 를 살펴본 후 대부분의 사용 사례에서 다음 코드 시퀀스가 ​​적합하다고 생각합니다.CancellationTokenSourceCancellationToken

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

m_kernelHandle상기 언급 된 내부 필드 백업 동기화 목적 WaitHandle모두 CTS 및 CT 클래스의 속성. 해당 속성에 액세스하는 경우에만 인스턴스화됩니다. 따라서 호출 dispose WaitHandle에서 구식 스레드 동기화에 사용 Task하지 않으면 효과가 없습니다.

물론, 당신 그것을 사용하는 경우 위의 다른 답변에서 제안한 것을 수행하고 호출 Dispose될 때까지 호출을 지연시켜야 합니다WaitHandle 핸들을 사용하여 작업이 완료로에 설명되어 있기 때문에, WaitHandle이 용 Windows API 문서 , 결과는 정의되지 않은 있습니다.


답변

내가 이것을 요청하고 많은 도움이되는 답변을 얻은 지 오랜 시간이 지났지 만 이것과 관련된 흥미로운 문제가 발생하여 여기에 다른 답변으로 게시 할 것이라고 생각했습니다.

CancellationTokenSource.Dispose()아무도 CTS의 Token부동산 을 취득하려고 시도하지 않을 것이라고 확신 할 때만 전화해야 합니다. 그렇지 않으면 인종이기 때문에 전화 해서는 안됩니다 . 예를 들어 여기를 참조하십시오.

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

이 문제에 대한 수정에서 이전에 수행 한 코드 cts.Cancel(); cts.Dispose(); 에 작성된 는 호출 된 cts.Cancel(); 취소 상태를 관찰하기 위해 취소 토큰을 얻으려고 시도하는 사람 이 불행히도 처리해야 합니다. 그들이 계획하고 있던 DisposeObjectDisposedExceptionOperationCanceledException

이 수정과 관련된 또 다른 주요 관찰 사항은 Tratcher에 의해 수행됩니다. “취소는 동일한 정리를 모두 수행하므로 취소되지 않는 토큰에만 처리가 필요합니다.” 즉, 그냥Cancel() , 처분 대신에하는 것만으로도 충분합니다!


답변

a CancellationTokenSource에 a 를 바인딩하는 스레드 안전 클래스를 만들었 으며 관련 완료 시 처리가 처리되도록 Task보장합니다 . 잠금을 사용하여 폐기 중 또는 폐기 후 취소되지 않도록합니다. 이것은 다음과 같은 문서 를 준수하기 위해 발생합니다 .CancellationTokenSourceTaskCancellationTokenSource

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클래스 의 주요 메소드 는 RunAsyncCancel입니다. 기본적으로 동시 작업은 허용되지 않습니다. 즉,RunAsync , 새 작업을 시작하기 전에 다시 하면 이전 작업 (아직 실행중인 경우)이 자동으로 취소되고 대기합니다.

이 클래스는 모든 종류의 응용 프로그램에서 사용할 수 있습니다. 기본 사용법은 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메소드는 extra CancellationToken를 인수로 받아들이며 내부적으로 작성된에 연결됩니다 CancellationTokenSource. 이 선택적 토큰을 제공하는 것은 고급 시나리오에서 유용 할 수 있습니다.