Kapan harus membatalkan CancurTokenSource?

163

Kelas CancellationTokenSourceini sekali pakai. Pandangan cepat di Reflector membuktikan penggunaan KernelEvent, sumber daya yang (tidak mungkin) dikelola. Karena CancellationTokenSourcetidak memiliki finalizer, jika kita tidak membuangnya, GC tidak akan melakukannya.

Di sisi lain, jika Anda melihat sampel yang terdaftar di artikel MSDN Pembatalan di Thread yang Dikelola , hanya satu cuplikan kode yang membuang token.

Apa cara yang tepat untuk membuangnya dalam kode?

  1. Anda tidak dapat membungkus kode dengan memulai tugas paralel usingAnda jika Anda tidak menunggu. Dan masuk akal untuk memiliki pembatalan hanya jika Anda tidak menunggu.
  2. Tentu saja Anda dapat menambahkan ContinueWithtugas dengan Disposepanggilan, tetapi apakah itu cara untuk pergi?
  3. Bagaimana dengan permintaan PLINQ yang dapat dibatalkan, yang tidak disinkronkan kembali, tetapi hanya melakukan sesuatu pada akhirnya? Katakan saja .ForAll(x => Console.Write(x))?
  4. Apakah bisa digunakan kembali? Dapatkah token yang sama digunakan untuk beberapa panggilan dan kemudian membuangnya bersama-sama dengan komponen host, misalkan kontrol UI?

Karena tidak memiliki sesuatu seperti Resetmetode untuk pembersihan IsCancelRequesteddan Tokenbidang saya kira itu tidak dapat digunakan kembali, sehingga setiap kali Anda memulai tugas (atau permintaan PLINQ), Anda harus membuat yang baru. Apakah itu benar Jika ya, pertanyaan saya adalah apa strategi yang tepat dan direkomendasikan untuk menangani Disposebanyak CancellationTokenSourcecontoh?

George Mamaladze
sumber

Jawaban:

82

Berbicara tentang apakah benar-benar perlu untuk memanggil Buang CancellationTokenSource... saya punya kebocoran memori di proyek saya dan ternyata CancellationTokenSourceitu masalahnya.

Proyek saya memiliki layanan, yang secara konstan membaca basis data dan menjalankan tugas yang berbeda, dan saya memberikan token pembatalan terkait kepada pekerja saya, jadi bahkan setelah mereka selesai memproses data, token pembatalan tidak dibuang, yang menyebabkan kebocoran memori.

Pembatalan MSDN di Thread yang Dikelola menyatakannya dengan jelas:

Perhatikan bahwa Anda harus memanggil Disposesumber token yang terhubung saat Anda selesai menggunakannya. Untuk contoh yang lebih lengkap, lihat Cara: Mendengarkan Beberapa Permintaan Pembatalan .

Saya menggunakan ContinueWithdalam implementasi saya.

Gruzilkin
sumber
14
Ini adalah kelalaian penting dalam jawaban yang diterima saat ini oleh Bryan Crosby - jika Anda membuat CTS tertaut , Anda berisiko kebocoran memori. Skenario ini sangat mirip dengan event handler yang tidak pernah tidak terdaftar.
Søren Boisen
5
Saya mengalami kebocoran karena masalah yang sama ini. Menggunakan profiler, saya bisa melihat pendaftaran callback memegang referensi ke instance CTS yang ditautkan. Memeriksa kode untuk implementasi Buang CTS di sini sangat berwawasan luas, dan menggarisbawahi perbandingan @ SørenBoisen dengan kebocoran registrasi event handler.
BitMask777
Komentar di atas mencerminkan keadaan diskusi dan jawaban lain oleh @Bryan Crosby diterima.
George Mamaladze
Dokumentasi pada tahun 2020 dengan jelas mengatakan: 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/standard/threading/…
Endrju
44

Saya tidak berpikir ada jawaban yang memuaskan. Setelah meneliti saya menemukan jawaban ini dari Stephen Toub ( referensi ):

Tergantung. Dalam. NET 4, CTS. Tujuan melayani dua tujuan utama. Jika WaitHandle dari PembatalanToken telah diakses (dengan malas mengalokasikannya), Buang akan membuang pegangan itu. Selain itu, jika CTS dibuat melalui metode CreateLinkedTokenSource, Buang akan memutuskan tautan CTS dari token yang ditautkan. Di .NET 4.5, Buang memiliki tujuan tambahan, yaitu jika CTS menggunakan Timer di bawah penutup (mis. CancelAfter dipanggil), Timer akan dibuang.

Ini sangat jarang untuk PembatalanToken.WaitHandle untuk digunakan, jadi membersihkan setelah itu biasanya bukan alasan bagus untuk menggunakan Buang. Namun, jika Anda membuat CTS Anda dengan CreateLinkedTokenSource, atau jika Anda menggunakan fungsi penghitung waktu CTS, akan lebih berdampak jika menggunakan Buang.

Bagian yang berani menurut saya adalah bagian yang penting. Dia menggunakan "lebih berdampak" yang membuatnya agak kabur. Saya menafsirkannya sebagai pemanggilan yang berarti Disposedalam situasi-situasi itu harus dilakukan, jika tidak menggunakan Disposetidak diperlukan.

Jesse Bagus
sumber
10
Lebih berdampak berarti CTS anak ditambahkan ke orangtua. Jika Anda tidak membuang anak, akan ada kebocoran jika orang tua berumur panjang. Jadi sangat penting untuk membuang yang terkait.
Grigory
26

Saya melihat di ILSpy untuk CancellationTokenSourcetetapi saya hanya bisa menemukan m_KernelEventyang sebenarnya ManualResetEvent, yang merupakan kelas pembungkus untuk WaitHandleobjek. Ini harus ditangani dengan benar oleh GC.

Bryan Crosby
sumber
7
Saya memiliki perasaan yang sama bahwa GC akan membersihkan itu semua. Saya akan mencoba memverifikasi itu. Mengapa Microsoft menerapkan membuang dalam kasus ini? Untuk menyingkirkan callback acara dan menghindari penyebaran ke generasi kedua GC mungkin. Dalam hal ini panggilan Buang adalah opsional - panggil jika Anda bisa, jika tidak abaikan saja. Bukan cara terbaik yang saya pikirkan.
George Mamaladze
4
Saya telah menyelidiki masalah ini. PembatalanTokenSource mengumpulkan sampah. Anda dapat membantu membuangnya di GEN 1 GC. Diterima
George Mamaladze
1
Saya melakukan investigasi yang sama ini secara independen dan sampai pada kesimpulan yang sama: buang jika Anda dengan mudah bisa, tetapi jangan khawatir mencoba melakukannya dalam kasus yang jarang-tetapi-bukan-tidak pernah terjadi di mana Anda telah mengirim Pembatalan. boondocks dan tidak ingin menunggu mereka untuk menulis kartu pos kembali memberitahu Anda mereka sudah selesai dengan itu. Ini akan terjadi sesekali karena sifat dari apa yang digunakan PembatalanToken, dan itu benar-benar oke, saya janji.
Joe Amenta
6
Komentar saya di atas tidak berlaku untuk sumber token yang ditautkan; Saya tidak dapat membuktikan bahwa tidak apa-apa untuk meninggalkan yang tidak diinginkan ini, dan kebijaksanaan di utas ini dan MSDN menunjukkan bahwa mungkin tidak.
Joe Amenta
23

Anda harus selalu membuang CancellationTokenSource.

Cara membuangnya tergantung pada skenario. Anda mengusulkan beberapa skenario berbeda.

  1. usinghanya berfungsi ketika Anda menggunakan CancellationTokenSourcepada beberapa pekerjaan paralel yang Anda tunggu. Jika itu senario Anda, maka hebat, itu metode termudah.

  2. Saat menggunakan tugas, gunakan ContinueWithtugas yang Anda inginkan CancellationTokenSource.

  3. Untuk plinq Anda dapat menggunakannya usingkarena Anda menjalankannya secara paralel tetapi menunggu semua pekerja yang berjalan paralel selesai.

  4. Untuk UI, Anda dapat membuat yang baru CancellationTokenSourceuntuk setiap operasi yang dapat dibatalkan yang tidak terkait dengan satu pemicu batal tunggal. Pertahankan List<IDisposable>dan tambahkan setiap sumber ke daftar, buang semuanya saat komponen Anda dibuang.

  5. Untuk utas, buat utas baru yang bergabung dengan semua utas pekerja dan tutup satu sumber saat semua utas pekerja selesai. Lihat Pembatalan Sumber Daya, Kapan membuang?

Selalu ada jalan. IDisposablecontoh harus selalu dibuang. Sampel sering tidak karena mereka baik sampel cepat untuk menunjukkan penggunaan inti atau karena menambahkan dalam semua aspek kelas yang diperlihatkan akan terlalu rumit untuk sampel. Sampel hanyalah sampel, tidak harus (atau bahkan biasanya) kode kualitas produksi. Tidak semua sampel dapat diterima untuk disalin ke dalam kode produksi apa adanya.

Samuel Neff
sumber
untuk poin 2, alasan apa pun yang tidak dapat Anda gunakan awaitpada tugas dan membuang CancertokenSource dalam kode yang datang setelah menunggu?
stijn
14
Ada banyak peringatan. Jika CTS dibatalkan saat awaitoperasi, Anda dapat melanjutkan karena OperationCanceledException. Anda mungkin akan menelepon Dispose(). Tetapi jika ada operasi yang masih berjalan dan menggunakan yang sesuai CancellationToken, token itu masih dilaporkan CanBeCanceledsebagai truesumber meskipun dibuang. Jika mereka mencoba mendaftarkan callback pembatalan, BOOM! , ObjectDisposedException. Cukup aman untuk menelepon Dispose()setelah operasi berhasil diselesaikan. Ini menjadi sangat rumit ketika Anda benar-benar perlu membatalkan sesuatu.
Mike Strobel
8
Diturunkan karena alasan yang diberikan oleh Mike Strobel - memaksakan aturan untuk selalu menelepon Buang dapat membawa Anda ke situasi berbulu ketika berhadapan dengan CTS dan Tugas karena sifatnya yang tidak sinkron. Aturannya seharusnya: selalu buang sumber token yang tertaut .
Søren Boisen
1
Tautan Anda menuju ke jawaban yang dihapus.
Dipotong
19

Jawaban ini masih muncul dalam pencarian Google, dan saya percaya jawaban yang dipilih tidak memberikan cerita lengkap. Setelah melihat kode sumber untuk CancellationTokenSource(CTS) dan CancellationToken(CT) saya percaya bahwa untuk sebagian besar kasus penggunaan urutan kode berikut baik-baik saja:

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

Bidang m_kernelHandleinternal yang disebutkan di atas adalah objek sinkronisasi yang mendukung WaitHandleproperti di kedua kelas CTS dan CT. Ini hanya dipakai jika Anda mengakses properti itu. Jadi, kecuali Anda menggunakan WaitHandlesinkronisasi sekolah lama dalam Taskpemanggilan panggilan Anda tidak akan berpengaruh.

Tentu saja, jika Anda sedang menggunakannya Anda harus melakukan apa yang disarankan oleh jawaban yang lain di atas dan keterlambatan panggilan Disposesampai setiap WaitHandleoperasi menggunakan pegangan yang lengkap, karena, seperti yang dijelaskan dalam API Windows dokumentasi untuk WaitHandle , hasilnya tidak terdefinisi.

jlyonsmith
sumber
7
Artikel MSDN Pembatalan di Thread yang Dikelola menyatakan: "Pendengar memantau nilai IsCancellationRequestedproperti token dengan polling, panggilan balik, atau gagang tunggu." Dengan kata lain: Ini mungkin bukan Anda (yaitu orang yang membuat permintaan async) yang menggunakan pegangan tunggu, mungkin pendengar (yaitu orang yang menjawab permintaan). Yang berarti Anda, sebagai orang yang bertanggung jawab untuk membuang, secara efektif tidak memiliki kendali atas apakah pegangan tunggu digunakan atau tidak.
herzbube
Menurut MSDN, panggilan balik terdaftar yang telah dikecualikan akan menyebabkan .Cancel untuk membuang. Kode Anda tidak akan memanggil .Dispose () jika ini terjadi. Callback harus berhati-hati untuk tidak melakukan ini, tetapi itu bisa terjadi.
Joseph Lennox
11

Sudah lama sejak saya menanyakan hal ini dan mendapatkan banyak jawaban yang bermanfaat tetapi saya menemukan masalah menarik terkait hal ini dan berpikir saya akan mempostingnya di sini sebagai jawaban lain:

Anda harus menelepon CancellationTokenSource.Dispose()hanya ketika Anda yakin tidak ada yang akan mencoba untuk mendapatkan Tokenproperti CTS . Kalau tidak, Anda tidak boleh menyebutnya, karena itu adalah ras. Misalnya, lihat di sini:

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

Dalam perbaikan untuk masalah ini, kode yang sebelumnya dilakukan cts.Cancel(); cts.Dispose();diedit hanya dilakukan cts.Cancel();karena ada orang yang kurang beruntung untuk mencoba mendapatkan token pembatalan untuk mengamati status pembatalannya setelah Dispose dipanggil, sayangnya juga perlu ditangani ObjectDisposedException- selain OperationCanceledExceptionyang mereka rencanakan.

Pengamatan kunci lain yang terkait dengan perbaikan ini dibuat oleh Tratcher: "Pembuangan hanya diperlukan untuk token yang tidak akan dibatalkan, karena pembatalan melakukan semua pembersihan yang sama." yaitu hanya melakukan Cancel()alih - alih membuang benar-benar cukup baik!

Tim Lovell-Smith
sumber
1

Saya membuat kelas thread-safe yang mengikat a CancellationTokenSourceke Task, dan menjamin bahwa CancellationTokenSourceakan dibuang ketika terkait Taskselesai. Menggunakan kunci untuk memastikan bahwa CancellationTokenSourcetidak akan dibatalkan selama atau setelah dibuang. Ini terjadi untuk kepatuhan dengan dokumentasi , yang menyatakan:

The DisposeMetode hanya harus digunakan ketika semua operasi lain pada CancellationTokenSourceobjek telah selesai.

Dan juga :

The DisposeMetode meninggalkan CancellationTokenSourcedalam keadaan tidak dapat digunakan.

Inilah kelasnya:

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

Metode utama CancelableExecutionkelas adalah RunAsyncdan Cancel. Secara default, operasi konkuren tidak diperbolehkan, artinya memanggil RunAsynckedua kalinya secara diam-diam akan membatalkan dan menunggu penyelesaian operasi sebelumnya (jika masih berjalan), sebelum memulai operasi baru.

Kelas ini dapat digunakan dalam aplikasi apa pun. Penggunaan utamanya adalah dalam aplikasi UI, di dalam formulir dengan tombol untuk memulai dan membatalkan operasi asinkron, atau dengan kotak daftar yang membatalkan dan memulai kembali operasi setiap kali item yang dipilih diubah. Ini adalah contoh dari kasus pertama:

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

The RunAsyncMetode menerima tambahan CancellationTokensebagai argumen, yang terkait dengan dibuat secara internal CancellationTokenSource. Memasok token opsional ini mungkin berguna dalam skenario tingkat lanjut.

Theodor Zoulias
sumber