Khi nào nên hủy CancellingTokenSource?


163

Các lớp học CancellationTokenSourcelà dùng một lần. Một cái nhìn nhanh trong Reflector chứng minh việc sử dụng KernelEvent, một nguồn tài nguyên (rất có thể) không được quản lý. Vì CancellationTokenSourcekhông có bộ hoàn thiện, nên nếu chúng tôi không loại bỏ nó, thì GC sẽ không làm điều đó.

Mặt khác, nếu bạn xem các mẫu được liệt kê trên Hủy bỏ bài viết MSDN trong Chủ đề được quản lý , chỉ có một đoạn mã loại bỏ mã thông báo.

Cách thích hợp để loại bỏ nó trong mã là gì?

  1. Bạn không thể gói mã bắt đầu tác vụ song song của mình usingnếu bạn không chờ đợi. Và nó có ý nghĩa để hủy bỏ chỉ khi bạn không chờ đợi.
  2. Tất nhiên bạn có thể thêm ContinueWithvào nhiệm vụ với một Disposecuộc gọi, nhưng đó có phải là cách để đi?
  3. Điều gì về các truy vấn PLINQ có thể hủy, không đồng bộ hóa trở lại, nhưng chỉ cần làm một cái gì đó ở cuối? Hãy nói .ForAll(x => Console.Write(x))?
  4. Có thể tái sử dụng? Có thể sử dụng cùng một mã thông báo cho một số cuộc gọi và sau đó loại bỏ nó cùng với thành phần máy chủ, giả sử kiểm soát giao diện người dùng?

Bởi vì nó không có một cái gì đó giống như một Resetphương pháp để dọn dẹp IsCancelRequestedTokentrường, tôi cho rằng nó không thể tái sử dụng, do đó, mỗi khi bạn bắt đầu một nhiệm vụ (hoặc truy vấn PLINQ), bạn nên tạo một cái mới. Có đúng không Nếu có, câu hỏi của tôi là chiến lược chính xác và được đề xuất để giải quyết Disposetrong nhiều CancellationTokenSourcetrường hợp đó là gì?

Câu trả lời:


81

Nói về việc có thực sự cần thiết phải gọi Vứt bỏ hay không CancellationTokenSource... Tôi đã bị rò rỉ bộ nhớ trong dự án của mình và hóa ra làCancellationTokenSource là vấn đề.

Dự án của tôi có một dịch vụ, liên tục đọc cơ sở dữ liệu và thực hiện các tác vụ khác nhau và tôi đã chuyển các mã thông báo hủy được liên kết cho các nhân viên của mình, vì vậy ngay cả khi họ đã xử lý xong dữ liệu, các mã thông báo hủy không được xử lý, gây rò rỉ bộ nhớ.

Hủy bỏ MSDN trong Chủ đề được quản lý nêu rõ:

Lưu ý rằng bạn phải gọi Disposenguồn mã thông báo được liên kết khi bạn hoàn thành nó. Để biết ví dụ đầy đủ hơn, hãy xem Cách: Nghe nhiều yêu cầu hủy .

Tôi đã sử dụng ContinueWithtrong việc thực hiện của tôi.


14
Đây là một thiếu sót quan trọng trong câu trả lời được chấp nhận hiện tại của Bryan Crosby - nếu bạn tạo CTS được liên kết , bạn có nguy cơ bị rò rỉ bộ nhớ. Kịch bản rất giống với các trình xử lý sự kiện không bao giờ được đăng ký.
Søren Boisen

5
Tôi đã bị rò rỉ do vấn đề tương tự. Sử dụng một trình lược tả tôi có thể thấy các đăng ký gọi lại giữ các tham chiếu đến các phiên bản CTS được liên kết. Kiểm tra mã cho việc thực hiện CTS Vứt bỏ ở đây là rất sâu sắc và nhấn mạnh @ SørenBoisen so với rò rỉ đăng ký xử lý sự kiện.
BitMask777 ngày

Nhận xét trên phản ánh trạng thái thảo luận là câu trả lời khác của @Bryan Crosby đã được chấp nhận.
George Mamaladze

Tài liệu năm 2020 ghi rõ: 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-us/dotnet/stiteria/threading/ Kẻ
Endrju

44

Tôi không nghĩ bất kỳ câu trả lời hiện tại nào là thỏa đáng. Sau khi nghiên cứu tôi tìm thấy câu trả lời này từ Stephen Toub ( tham khảo ):

Nó phụ thuộc. Trong .NET 4, CTS.Dispose phục vụ hai mục đích chính. Nếu WaitHandle của CancellingToken đã được truy cập (do đó phân bổ nó một cách lười biếng), Vứt bỏ sẽ loại bỏ xử lý đó. Ngoài ra, nếu CTS được tạo thông qua phương thức CreatLinkedTokenSource, Dispose sẽ hủy liên kết CTS khỏi các mã thông báo mà nó được liên kết đến. Trong .NET 4.5, Dispose có một mục đích bổ sung, đó là nếu CTS sử dụng Timer dưới nắp (ví dụ: Hủy sau khi được gọi), Timer sẽ bị loại bỏ.

Rất hiếm khi Canc CancToken.WaitHandle được sử dụng, vì vậy việc dọn dẹp sau đó thường không phải là lý do tuyệt vời để sử dụng Vứt bỏ. Tuy nhiên, nếu bạn đang tạo CTS của mình bằng CreatLinkedTokenSource hoặc nếu bạn đang sử dụng chức năng hẹn giờ của CTS, việc sử dụng Vứt bỏ sẽ có tác động hơn.

Phần táo bạo tôi nghĩ là phần quan trọng. Anh ta sử dụng "tác động mạnh hơn" khiến nó hơi mơ hồ. Tôi đang giải thích nó có nghĩa là gọi Disposetrong những tình huống đó nên được thực hiện, nếu không sử dụng Disposelà không cần thiết.


10
Tác động mạnh hơn có nghĩa là CTS con được thêm vào cha mẹ. Nếu bạn không vứt bỏ con, sẽ có một rò rỉ nếu cha mẹ sống lâu. Vì vậy, nó là rất quan trọng để loại bỏ những người liên kết.
Grigory

26

Tôi đã xem xét ILSpy CancellationTokenSourcenhưng tôi chỉ có thể tìm thấy m_KernelEventcái thực sự ManualResetEventlà một lớp bao bọc cho một WaitHandleđối tượng. Điều này nên được xử lý đúng bởi GC.


7
Tôi có cùng cảm giác rằng GC sẽ dọn sạch tất cả. Tôi sẽ cố gắng xác minh điều đó. Tại sao Microsoft thực hiện xử lý trong trường hợp này? Để thoát khỏi các cuộc gọi lại sự kiện và tránh sự lan truyền đến thế hệ thứ hai có lẽ. Trong trường hợp này, gọi Dispose là tùy chọn - gọi nó nếu bạn có thể, nếu không chỉ cần bỏ qua nó. Không phải là cách tốt nhất tôi nghĩ.
George Mamaladze

4
Tôi đã điều tra vấn đề này. CancellingTokenSource được thu gom rác. Bạn có thể giúp loại bỏ để làm điều đó trong GEN 1 GC. Đã được chấp nhận.
George Mamaladze

1
Tôi đã thực hiện điều tra tương tự một cách độc lập và đi đến cùng một kết luận: loại bỏ nếu bạn dễ dàng có thể, nhưng đừng băn khoăn cố gắng làm điều đó trong những trường hợp hiếm hoi nhưng không phải là chưa từng nghe thấy mà bạn đã gửi Hủy bỏ. boondocks và không muốn đợi họ viết lại một tấm bưu thiếp cho bạn biết họ đã hoàn thành nó. Điều này sẽ xảy ra mọi lúc mọi nơi vì bản chất của CancellingToken được sử dụng cho mục đích gì, và nó thực sự ổn, tôi hứa.
Joe Amenta

6
Nhận xét trên của tôi không áp dụng cho các nguồn mã thông báo được liên kết; Tôi không thể chứng minh rằng không sao để những điều này không bị ảnh hưởng, và sự khôn ngoan trong chủ đề này và MSDN cho thấy rằng nó có thể không.
Joe Amenta 8/07/2015

23

Bạn nên luôn luôn vứt bỏ CancellationTokenSource .

Làm thế nào để loại bỏ nó phụ thuộc chính xác vào kịch bản. Bạn đề xuất một số kịch bản khác nhau.

  1. usingchỉ hoạt động khi bạn đang sử dụng CancellationTokenSourcetrên một số công việc song song mà bạn đang chờ. Nếu đó là senario của bạn, thì thật tuyệt, đó là phương pháp dễ nhất.

  2. Khi sử dụng các tác vụ, sử dụng một ContinueWithtác vụ như bạn đã chỉ định để xử lý CancellationTokenSource.

  3. Đối với plinq, bạn có thể sử dụng usingvì bạn đang chạy song song nhưng chờ tất cả các công nhân chạy song song kết thúc.

  4. Đối với UI, bạn có thể tạo một cái mới CancellationTokenSourcecho mỗi thao tác có thể hủy mà không bị ràng buộc với một kích hoạt hủy duy nhất. Duy trì a List<IDisposable>và thêm từng nguồn vào danh sách, loại bỏ tất cả chúng khi thành phần của bạn bị loại bỏ.

  5. Đối với các luồng, tạo một luồng mới kết hợp tất cả các luồng worker và đóng nguồn đơn khi tất cả các luồng worker hoàn thành. Xem CancellingTokenSource, khi nào cần xử lý?

Luôn luôn có một cách. IDisposabletrường hợp nên luôn luôn được xử lý. Các mẫu thường không vì chúng là các mẫu nhanh để hiển thị mức sử dụng cốt lõi hoặc bởi vì việc thêm vào tất cả các khía cạnh của lớp được trình diễn sẽ quá phức tạp đối với một mẫu. Mẫu chỉ là một mẫu, không nhất thiết (hoặc thậm chí thường là) mã chất lượng sản xuất. Không phải tất cả các mẫu đều được chấp nhận để được sao chép vào mã sản xuất.


đối với điểm 2, bất kỳ lý do nào bạn không thể sử dụng awaitcho nhiệm vụ và loại bỏ CancellingTokenSource trong mã xuất hiện sau khi chờ đợi?
stijn

14
Có hãy cẩn thận. Nếu CTS bị hủy trong khi bạn awaithoạt động, bạn có thể tiếp tục do OperationCanceledException. Sau đó bạn có thể gọi Dispose(). Nhưng nếu có các hoạt động vẫn đang chạy và sử dụng tương ứng CancellationToken, mã thông báo đó vẫn báo cáo CanBeCanceledtruemặc dù nguồn được xử lý. Nếu họ cố gắng đăng ký gọi lại hủy, BÙM! , ObjectDisposedException. Nó đủ an toàn để gọi Dispose()sau khi hoàn thành (các) hoạt động. Nó thực sự khó khăn khi bạn thực sự cần phải hủy bỏ một cái gì đó.
Mike Strobel

8
Bị từ chối vì những lý do được đưa ra bởi Mike Strobel - buộc một quy tắc luôn gọi Dispose có thể khiến bạn rơi vào tình huống đầy lông khi xử lý CTS và Nhiệm vụ do tính chất không đồng bộ của chúng. Thay vào đó, quy tắc nên là: luôn loại bỏ các nguồn mã thông báo được liên kết .
Søren Boisen

1
Liên kết của bạn đi đến một câu trả lời bị xóa.
Đã xem

19

Câu trả lời này vẫn đang xuất hiện trong các tìm kiếm của Google và tôi tin rằng câu trả lời được bình chọn không đưa ra câu chuyện đầy đủ. Sau khi xem qua mã nguồn cho CancellationTokenSource(CTS) và CancellationToken(CT) tôi tin rằng đối với hầu hết các trường hợp sử dụng, chuỗi mã sau đây đều ổn:

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

Trường m_kernelHandlebên trong được đề cập ở trên là đối tượng đồng bộ hóa sao lưu thuộc WaitHandletính trong cả hai lớp CTS và CT. Nó chỉ được khởi tạo nếu bạn truy cập vào tài sản đó. Vì vậy, trừ khi bạn đang sử dụng WaitHandlecho một số đồng bộ hóa chủ đề trường học cũ trongTask gọi điện thoại sẽ không có hiệu lực.

Tất nhiên, nếu bạn đang sử dụng nó, bạn nên thực hiện những gì được đề xuất bởi các câu trả lời khác ở trên và trì hoãn cuộc gọi Disposecho đến khi mọi WaitHandlethao tác sử dụng tay cầm hoàn tất, bởi vì, như được mô tả trong tài liệu API của Windows cho WaitHandle , kết quả không được xác định.


7
Hủy bỏ bài viết MSDN trong Chủ đề được quản lý nêu rõ: "Người nghe theo dõi giá trị IsCancellationRequestedtài sản của mã thông báo bằng cách bỏ phiếu, gọi lại hoặc xử lý chờ." Nói cách khác: Có thể không phải bạn (tức là người đưa ra yêu cầu không đồng bộ), người sử dụng tay cầm chờ, đó có thể là người nghe (tức là người trả lời yêu cầu). Điều đó có nghĩa là bạn, với tư cách là người chịu trách nhiệm xử lý, thực sự không kiểm soát được liệu tay cầm chờ có được sử dụng hay không.
herzbube

Theo MSDN, các cuộc gọi lại đã đăng ký bị loại trừ sẽ khiến. Hủy bỏ. Mã của bạn sẽ không gọi .Dispose () nếu điều này xảy ra. Các cuộc gọi lại nên cẩn thận không làm điều này, nhưng nó có thể xảy ra.
Joseph Lennox

11

Đã lâu rồi tôi mới hỏi điều này và nhận được nhiều câu trả lời hữu ích nhưng tôi đã gặp một vấn đề thú vị liên quan đến vấn đề này và nghĩ rằng tôi sẽ đăng nó ở đây như một câu trả lời khác:

Bạn chỉ nên gọi CancellationTokenSource.Dispose()khi bạn chắc chắn rằng không ai sẽ cố lấy Tokentài sản của CTS . Nếu không, bạn không nên gọi nó, bởi vì nó là một cuộc đua. Ví dụ, xem tại đây:

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

Để khắc phục sự cố này, mã mà trước đây đã cts.Cancel(); cts.Dispose();được chỉnh sửa để thực hiện cts.Cancel();vì bất kỳ ai không may mắn cố gắng lấy mã thông báo hủy để quan sát trạng thái hủy của nó sau khi Dispose được gọi cũng sẽ không may xử lý ObjectDisposedException- ngoài việcOperationCanceledException mà họ đã lên kế hoạch cho.

Một quan sát quan trọng khác liên quan đến sửa lỗi này được thực hiện bởi Tratcher: "Việc xử lý chỉ được yêu cầu đối với các mã thông báo sẽ không bị hủy, vì hủy bỏ tất cả cùng một hoạt động dọn dẹp." tức là chỉ cần làm Cancel()thay vì xử lý là thực sự đủ tốt!


1

Tôi đã tạo một lớp an toàn luồng liên kết a CancellationTokenSourcevới a Taskvà đảm bảo rằng nó CancellationTokenSourcesẽ được xử lý khi liên kết của nó Taskhoàn thành. Nó sử dụng khóa để đảm bảo rằng CancellationTokenSourcesẽ không bị hủy trong hoặc sau khi nó đã được xử lý. Điều này xảy ra để tuân thủ các tài liệu , nói rằng:

Các Disposephương pháp duy nhất phải được sử dụng khi tất cả các hoạt động khác trên CancellationTokenSourceđối tượng đã hoàn thành.

cũng :

Các Disposephương pháp rời khỏi CancellationTokenSourcetrong tình trạng không sử dụng được.

Đây là lớp học:

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;
}

Các phương thức chính của CancelableExecutionlớp là RunAsyncCancel. Theo mặc định, các hoạt động đồng thời không được phép, có nghĩa là gọiRunAsync lần thứ hai sẽ âm thầm hủy và chờ hoàn thành thao tác trước đó (nếu nó vẫn chạy), trước khi bắt đầu thao tác mới.

Lớp này có thể được sử dụng trong các ứng dụng dưới mọi hình thức. Mặc dù, cách sử dụng chính của nó là trong các ứng dụng UI, bên trong các biểu mẫu có các nút để bắt đầu và hủy hoạt động không đồng bộ hoặc với hộp danh sách hủy và khởi động lại hoạt động mỗi khi thay đổi mục được chọn. Dưới đây là một ví dụ về trường hợp đầu tiên:

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();
}

Các RunAsyncphương pháp chấp nhận thêm CancellationTokennhư là đối số, đó là liên quan đến nội bộ tạo ra CancellationTokenSource. Cung cấp mã thông báo tùy chọn này có thể hữu ích trong các tình huống tiến bộ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.