CancellationTokenSource를 언제 처분해야합니까?


163

수업 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인스턴스 를 처리하기위한 정확하고 권장되는 전략은 무엇 입니까?

답변:


82

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

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

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

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

ContinueWith구현에 사용 했습니다.


14
Bryan Crosby가 현재 허용하는 답변에서 중요한 누락입니다. 연결된 CTS 를 만들면 메모리 누수가 발생할 수 있습니다. 이 시나리오는 등록되지 않은 이벤트 핸들러와 매우 유사합니다.
Søren Boisen 2016 년

5
이 같은 문제로 인해 누수가 발생했습니다. 프로파일 러를 사용하면 연결된 CTS 인스턴스에 대한 참조를 보유하는 콜백 등록을 볼 수 있습니다. 여기 에서 CTS Dispose 구현에 대한 코드를 조사하는 것은 매우 통찰력이 있으며 @ SørenBoisen과 이벤트 핸들러 등록 누수 비교를 강조합니다.
BitMask777

위의 의견은 토론 상태가 @Bryan Crosby의 다른 답변이 수락되었음을 반영합니다.
George Mamaladze

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 ( 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하지 않아도됩니다.


10
더 영향력이 큰 것은 하위 CTS가 상위 CTS에 추가됨을 의미합니다. 자녀를 처분하지 않으면 부모가 오래 살면 누수가 발생합니다. 따라서 연결된 것을 폐기하는 것이 중요합니다.
Grigory

26

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


7
나는 GC가 그 모든 것을 정리할 것이라는 같은 느낌을 가지고 있습니다. 나는 그것을 확인하려고 노력할 것이다. 이 경우 Microsoft는 왜 폐기를 구현합니까? 이벤트 콜백을 없애고 2 세대 GC 로의 전파를 피하려면 이 경우 Dispose 호출은 선택 사항입니다. 무시할 수있는 것이 아니라 가능하면 호출하십시오. 내가 생각하는 가장 좋은 방법은 아닙니다.
George Mamaladze

4
이 문제를 조사했습니다. CancellationTokenSource는 가비지 수집을 가져옵니다. GEN 1 GC에서 처리하는 데 도움이 될 수 있습니다. 받아 들였습니다.
George Mamaladze

1
나는이 동일한 조사를 독립적으로 수행했으며 같은 결론에 이르렀습니다. 쉽게 할 수는 있지만 취소 토큰을 보낸 드문 일이지만 결코 들리지 않는 경우에는 그렇게하지 마십시오. boondocks 그리고 그들이 완료되었음을 알리는 엽서를 다시 쓸 때까지 기다리기를 원하지 않습니다. 이것은 매번 일어날 것이고 CancellationToken이 사용되는 특성으로 인해 정말 좋습니다. 약속합니다.
Joe Amenta 2016 년

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해당 토큰은 여전히 소스가 폐기 된 CanBeCanceled것으로 보고합니다 true. 취소 콜백을 등록하려고하면 BOOM! , ObjectDisposedException. 작업이 Dispose()성공적으로 완료된 후 전화를 걸면 안전 합니다. 실제로 무언가를 취소해야 할 때 정말 까다로워 집니다 .
Mike Strobel

8
Mike Strobel이 제공 한 이유에 따라 하향 조정-규칙을 항상 호출하도록 강제 적용 Dispose는 비동기 특성으로 인해 CTS 및 작업을 처리 할 때 털이 많은 상황에 처할 수 있습니다. 규칙은 다음과 같아야합니다. 항상 링크 처리 토큰 소스를 .
Søren Boisen 2016 년

1
링크가 삭제 된 답변으로 이동합니다.
21:40에

19

이 답변은 여전히 ​​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 문서 , 결과는 정의되지 않은 있습니다.


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(); 취소 상태를 관찰하기 위해 취소 토큰을 얻으려고 시도하는 사람 이 불행히도 처리해야 합니다. 그들이 계획하고 있던 DisposeObjectDisposedExceptionOperationCanceledException

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


1

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. 이 선택적 토큰을 제공하는 것은 고급 시나리오에서 유용 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.