CancellationTokenSource ne zaman imha edilir?


163

Sınıf CancellationTokenSourcetek kullanımlıktır. Reflector'a hızlı bir bakış KernelEvent, (büyük olasılıkla) yönetilmeyen bir kaynağın kullanımını kanıtlar . Yana CancellationTokenSourcehiçbir finalizer vardır bunu elden yoksa, GC yapmayacağım.

Öte yandan, Yönetilen İş Parçacıklarında İptal adlı MSDN makalesinde listelenen örneklere bakarsanız , belirtecin yalnızca bir kod snippet'i kullanılır.

Kodda elden çıkarmanın uygun yolu nedir?

  1. Beklemezseniz paralel görevinize başlayarak kodu saramazsınız using. Ve sadece beklemezseniz iptal etmek mantıklıdır.
  2. Tabii ki ContinueWithbir Disposeçağrı ile göreve ekleyebilirsiniz , ama bu gitmek için bir yol mu?
  3. Geri senkronize olmayan, ancak sonunda bir şey yapan iptal edilebilir PLINQ sorgularına ne olacak? Diyelim .ForAll(x => Console.Write(x))?
  4. Tekrar kullanılabilir mi? Aynı simge birden fazla çağrı için kullanılabilir ve ardından ana bilgisayar bileşeniyle birlikte imha edilebilir mi, diyelim ki UI kontrolü?

ResetTemizleme IsCancelRequestedve Tokenalan için bir yöntem gibi bir şey olmadığından, yeniden kullanılabilir olmadığını varsayalım, bu nedenle her görev (veya PLINQ sorgusu) başlattığınızda yeni bir tane oluşturmanız gerekir. Bu doğru mu? Cevabınız evet ise sorum şu Dispose: Bu birçok CancellationTokenSourceörnekle başa çıkmak için doğru ve önerilen strateji nedir?

Yanıtlar:


82

Dispose on'u çağırmanın gerçekten gerekli olup olmadığından bahsetmişken CancellationTokenSource... Projemde bir bellek sızıntısı vardı CancellationTokenSourceve sorun çıktı.

Projemde sürekli veri tabanı okuyan ve farklı görevleri işleyen bir hizmet var ve bağlantılı iptal belirteçlerini işçilerime aktarıyordum, bu yüzden veri işlemeyi bitirdikten sonra iptal belirteçleri atılmadı, bu da bellek sızıntısına neden oldu.

Yönetilen Konulardaki MSDN İptali bunu açıkça belirtiyor:

İşiniz Disposebittiğinde bağlı simge kaynağını çağırmanız gerektiğine dikkat edin . Daha eksiksiz bir örnek için bkz. Nasıl Yapılır: Birden Çok İptal İsteğini Dinleme .

Kullandığım ContinueWithbenim uygulamasında.


14
Bu, Bryan Crosby tarafından kabul edilen geçerli cevapta önemli bir eksikliktir - bağlantılı bir CTS oluşturursanız, bellek sızıntısı riskiyle karşı karşıya kalırsınız. Senaryo, hiç kayıt olmamış olay işleyicilerine çok benzer.
Søren Boisen

5
Aynı sorun yüzünden bir sızıntı yaşadım. Bir profil oluşturucu kullanarak bağlantılı CTS örneklerine referanslar içeren geri arama kayıtlarını görebiliyordum. Burada CTS Dispose uygulaması için kodu incelemek çok anlayışlıydı ve @ SørenBoisen'ın olay işleyici kayıt sızıntılarına kıyasla altını çizdi.
BitMask777

Yukarıdaki yorumlar tartışma durumunu yansıtır @Bryan Crosby'nin diğer yanıtı kabul edildi.
George Mamaladze

2020'deki belgeler açıkça şunu söylüyor: 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

Mevcut cevapların hiçbirinin tatmin edici olduğunu düşünmüyordum. Araştırdıktan sonra Stephen Toub'dan bu cevabı buldum ( referans ):

Değişir. .NET 4'te, CTS.Dispose iki temel amaca hizmet etti. CancellationToken'ın WaitHandle'ına erişildiyse (böylece tembel bir şekilde tahsis ettiyse), bu sapı imha eder. Ayrıca, CTS CreateLinkedTokenSource yöntemiyle oluşturulduysa Dispose, CTS'nin bağlı olduğu belirteçlerle bağlantısını keser. .NET 4.5'te Dispose'in ek bir amacı vardır, yani CTS kapakların altında bir Zamanlayıcı kullanırsa (örn. CancelAfter çağrılır), Zamanlayıcı Atılır.

CancellationToken.WaitHandle'ın kullanılması çok nadirdir, bu nedenle genellikle Dispose kullanmak için harika bir neden değildir. Bununla birlikte, CTS'nizi CreateLinkedTokenSource ile oluşturuyorsanız veya CTS'nin zamanlayıcı işlevini kullanıyorsanız Dispose'i kullanmak daha etkili olabilir.

Bence cesur kısım önemli kısım. "Daha etkili" kullanır, bu da onu biraz belirsiz bırakır. Bunu Dispose, bu durumlarda çağrının yapılması gerektiği şeklinde yorumluyorum , aksi takdirde kullanmak Disposegerekli değildir.


10
Daha etkili olan, ebeveyn CTS'ye alt CTS'nin eklendiği anlamına gelir. Çocuğu elden çıkarmazsanız, ebeveyn uzun süre yaşıyorsa bir sızıntı olacaktır. Bu nedenle bağlantılı olanları atmak çok önemlidir.
Grigory

26

ILSpy için bir göz attım CancellationTokenSourceama sadece m_KernelEventbir ManualResetEventolanı, bir WaitHandlenesne için bir sarıcı sınıf olduğunu bulabilirsiniz . Bu GC tarafından uygun şekilde ele alınmalıdır.


7
GC'nin hepsini temizleyeceği hissine sahibim. Bunu doğrulamaya çalışacağım. Microsoft bu durumda neden imha ediyor? Olay geri çağrılarından kurtulmak ve muhtemelen ikinci nesil GC'ye yayılmayı önlemek için. Bu durumda Dispose çağrısı isteğe bağlıdır - eğer görmezden geliyorsanız, mümkünse arayın. Bence en iyi yol değil.
George Mamaladze

4
Bu sorunu araştırdım. TokenSource çöpleri toplar. GEN 1 GC'de atma konusunda yardımcı olabilirsiniz. Kabul edilmiş.
George Mamaladze

1
Aynı soruşturmayı bağımsız olarak yaptım ve aynı sonuca vardım: Kolayca yapabiliyorsanız imha edin, ancak bir İptal gönderdiğiniz nadir ancak duyulmamış vakalarda bunu yapmaya çalışmaktan korkmayın. ve bu işin bittiğini söyleyen bir kartpostal yazmalarını beklemek istemiyorum. Bu şimdi ve sonra CancellationToken için kullanılan ne doğası nedeniyle olacak, ve gerçekten sorun değil, söz veriyorum.
Joe Amenta

6
Yukarıdaki yorumum bağlantılı jeton kaynakları için geçerli değildir; Ben bu maruz bırakılmasının TAMAM olduğunu kanıtlayamadım ve bu konu ve MSDN bilgelik bunun olmayabilir olduğunu göstermektedir.
Joe Amenta

23

Her zaman imha etmelisin CancellationTokenSource.

Nasıl imha edileceği tam olarak senaryoya bağlıdır. Birkaç farklı senaryo önerirsiniz.

  1. usingyalnızca CancellationTokenSourcebeklediğiniz paralel işleri kullandığınızda çalışır. Eğer senaryonuz buysa, o zaman harika, en kolay yöntem.

  2. Görevleri kullanırken ContinueWith, atmak için belirttiğiniz gibi bir görev kullanın CancellationTokenSource.

  3. Plinq için usingparalel çalıştığınız için kullanabilirsiniz, ancak tüm paralel çalışan işçilerin bitmesini bekleyebilirsiniz .

  4. Kullanıcı arayüzü için, CancellationTokenSourcetek bir iptal tetikleyicisine bağlı olmayan her iptal edilebilir işlem için yeni bir tane oluşturabilirsiniz . A öğesini List<IDisposable>saklayın ve her kaynağı listeye ekleyin ve bileşeniniz atandığında hepsini atın.

  5. İş parçacıkları için, tüm çalışan iş parçacıkları tamamlandığında tüm çalışan iş parçacıklarını birleştiren ve tek kaynağı kapatan yeni bir iş parçacığı oluşturun. Bkz CancellationTokenSource, zaman elden çıkarma?

Her zaman bir yol vardır. IDisposableörnekler her zaman atılmalıdır. Örnekler çoğunlukla çekirdek kullanımını göstermek için hızlı örnekler oldukları veya gösterilmekte olan sınıfın tüm yönlerine eklenmesinin bir örnek için aşırı karmaşık olacağı için yapmazlar. Örnek, üretim kalitesi kodu olması gerekmeyen (hatta genellikle) bir numunedir. Tüm numunelerin olduğu gibi üretim koduna kopyalanması kabul edilemez.


2. nokta awaitiçin, görevde kullanamazsınız ve CancellationTokenSource'u beklemeden sonra gelen koda atamazsınız?
stijn

14
Uyarılar var. Bir işlem sırasında CTS iptal edilirse, awaitbir OperationCanceledException. Daha sonra arayabilirsiniz Dispose(). Ama operasyonlar hala çalışıyor ve ilgili kullanarak varsa CancellationToken, o simge hala raporlarını CanBeCanceledolarak truekaynak elden çıkarılan olsa. Bir iptal geri çağrısı kaydettirmeye çalışırlarsa, BOOM! , ObjectDisposedException. Dispose()İşlem (ler) başarıyla tamamlandıktan sonra arayabilecek kadar güvenlidir . Bir şeyi gerçekten iptal etmeniz gerektiğinde gerçekten zor olur.
Mike Strobel

8
Mike Strobel tarafından verilen nedenlerden ötürü - her zaman Dispose'i çağırmak için bir kuralı zorlamak, zaman uyumsuz doğaları nedeniyle CTS ve Görev ile uğraşırken sizi kıllı durumlara sokabilir. Kural bunun yerine şöyle olmalıdır: her zaman bağlı simge kaynaklarını imha edin .
Søren Boisen

1
Bağlantınız silinen bir cevaba gider.
Trisped

19

Bu yanıt hala Google aramalarında geliyor ve oylanan cevabın tüm hikayeyi vermediğine inanıyorum. (CTS) ve (CT) için kaynak koduna baktıktan sonra , çoğu kullanım durumunda aşağıdaki kod sırasının iyi olduğuna inanıyorum:CancellationTokenSourceCancellationToken

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

m_kernelHandleYukarıda belirtilen, iç alan destek senkronizasyon amacı, WaitHandleher ikisi de CTS ve BT sınıflarında özelliği. Yalnızca bu özelliğe eriştiğinizde somutlaştırılır. Bu nedenle, arama kullanımınızda WaitHandlebazı eski okul iplik senkronizasyonu için kullanmadığınız sürece Taskhiçbir etkisi olmayacaktır.

Tabii ki, eğer edilmektedir sen ve gecikme çağrı yukarıdaki diğer cevaplar önerdiği ne yapmalıyım bunu kullanarak Disposeherhangi dek WaitHandlekolu kullanarak işlemleri tamamlandıktan olarak açıklanan, çünkü WaitHandle Windows API belgelerine , sonuç tanımsız olduğunu.


7
Yönetilen Konulardaki MSDN makalesinde İptal : "Dinleyiciler IsCancellationRequested, belirteç özelliğinin değerini yoklama, geri arama veya bekleme tutamacıyla izler." Başka bir deyişle: Bekleme tutamacını kullanan siz (yani zaman uyumsuzluk isteği yapan kişi) olmayabilir, dinleyici de olabilir (yani isteği yanıtlayan kişi). Bu, atmadan sorumlu olan olarak, bekleme kolunun kullanılıp kullanılmadığı üzerinde etkili bir şekilde kontrol sahibi olmadığınız anlamına gelir.
herzbube

MSDN'ye göre, istisna olan kayıtlı geri aramalar .Cancel'in atmasına neden olacaktır. Bu durumda, kodunuz .Dispose () öğesini çağırmaz. Geri aramalar bunu yapmamaya dikkat etmelidir, ancak olabilir.
Joseph Lennox

11

Bunu sorduğumdan ve çok yararlı cevaplar aldığımdan beri uzun zaman oldu, ancak bununla ilgili ilginç bir sorunla karşılaştım ve burada başka bir cevap olarak göndereceğimi düşündüm:

CancellationTokenSource.Dispose()Sadece kimsenin CTS'nin Tokenmülkünü almaya çalışmadığından emin olduğunuzda aramalısınız . Aksi takdirde gereken değil bu bir yarış olduğu için, diyoruz. Örneğin, buraya bakın:

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

Bu sorunun düzeltilmesinde, daha önce yapılan kod cts.Cancel(); cts.Dispose();, sadece çağrıldıktan sonracts.Cancel(); iptal durumunu gözlemlemek için iptal jetonunu almaya çalışmak için şanssız olduğu için yapılacak şekilde düzenlendi , maalesef ele alınması gerekiyor . planlıyorlardı. DisposeObjectDisposedExceptionOperationCanceledException

Bu düzeltmeyle ilgili bir başka önemli gözlem Tratcher tarafından yapılır: "İptal işlemi aynı temizleme işleminin tümünü yaptığı için iptal edilmeyen jetonlar için atılması gerekir." yani sadece Cancel()atmak yerine yapmak gerçekten çok iyi!


1

Ben bağlandığı bir bir iş parçacığı güvenli sınıf yapılan CancellationTokenSourcebir etmek Taskve garanti o CancellationTokenSourcezaman onun ilişkili bertaraf edilecektir Tasktamamlanana. CancellationTokenSourceBertarafı sırasında veya sonrasında iptal edilmemesini sağlamak için kilitler kullanır . Bu, belgelere uymak için gerçekleşir , şunları belirtir:

DisposeTüm diğer işlemler sırasında yöntemi yalnızca kullanılan olmalıdır CancellationTokenSourcenesne tamamladık.

Ve ayrıca :

DisposeYöntem yaprak CancellationTokenSourcekullanılamaz bir halde bulunur.

İşte sınıf:

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

CancelableExecutionSınıfın birincil yöntemleri RunAsyncve Cancel. Varsayılan olarak, eşzamanlı işlemlere izin verilmez; bu RunAsync, yeni bir işleme başlamadan önce ikinci kez çağırmanın sessizce iptal edeceğini ve önceki işlemin (hala çalışıyorsa) tamamlanmasını bekleyeceğini gösterir.

Bu sınıf her türlü uygulamada kullanılabilir. Birincil kullanımı, UI uygulamalarında, eşzamansız bir işlemi başlatmak ve iptal etmek için düğmelere sahip formların içinde veya seçilen öğe her değiştiğinde bir işlemi iptal eden ve yeniden başlatan bir liste kutusuyla. İlk durumun bir örneği:

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

RunAsyncYöntemi bir ekstra kabul CancellationTokendahili olarak oluşturulan bağlıdır argüman olarak CancellationTokenSource. Bu isteğe bağlı belirtecin sağlanması, ilerleyen senaryolarda yararlı olabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.