Коли розпоряджатися CancellationTokenSource?


163

Клас CancellationTokenSourceодноразовий. Швидкий погляд у Reflector доводить використання KernelEvent(дуже ймовірно) некерованого ресурсу. З тих пірCancellationTokenSource немає фіналізатора, якщо ми не розпорядимось цим, GC не зробить цього.

З іншого боку, якщо ви подивитеся на зразки, перелічені у статті MSDN Скасування в керованих нитках , токена має лише один фрагмент коду.

Який правильний спосіб розпоряджатися ним у коді?

  1. Ви не можете обернути код, починаючи свою паралельну задачу using якщо його не чекаєте. І має сенс мати скасування, лише якщо ви не чекаєте.
  2. Звичайно, ви можете додати ContinueWithдо завдання за допомогоюDispose дзвінка, але це шлях?
  3. А як щодо скасовуваних запитів PLINQ, які не синхронізуються назад, а просто роблять щось наприкінці? Скажімо.ForAll(x => Console.Write(x)) ?
  4. Це багаторазове використання? Чи може той же маркер використовуватись для декількох викликів, а потім розпоряджатися ним разом із хост-компонентом, скажімо, керуванням інтерфейсом користувача?

Оскільки у нього немає нічого подібного до Resetметоду очищення IsCancelRequestedта Tokenполя, я вважаю, що він не може бути використаний повторно, тому кожен раз, коли ви запускаєте завдання (або запит PLINQ), ви повинні створювати нове. Це правда? Якщо так, моє запитання полягає в тому, якою є правильна та рекомендована стратегія вирішення Disposeцих багатьох CancellationTokenSourceвипадків?

Відповіді:


82

Якщо говорити про те, чи дійсно потрібно зателефонувати на Dispose on CancellationTokenSource... У мене в проекті просочилася пам'ять, і виявилося, що CancellationTokenSourceце проблема.

У моєму проекті є служба, яка постійно читає базу даних і виконує різні завдання, і я передав пов'язані жетони скасування моїм працівникам, тому навіть після того, як вони закінчили обробляти дані, жетони скасування не були видалені, що спричинило витік пам'яті.

Скасування MSDN в керованих нитках чітко зазначає:

Зауважте, що вам потрібно зателефонувати Disposeна пов'язане джерело токенів, коли ви закінчите з ним. Для більш повного прикладу див. Як: Прослуховування кількох запитів на скасування .

Я використовував ContinueWithу своїй реалізації.


14
Це важливий недолік у прийнятій досі Брайаном Кросбі відповіді - якщо ви створите пов'язаний CTS, ви ризикуєте витік пам'яті. Сценарій дуже схожий на обробників подій, які ніколи не зареєстровані.
Søren Boisen

5
У мене витік через цю саму проблему. Використовуючи профілер, я міг бачити реєстрації зворотних викликів, що містять посилання на пов'язані екземпляри CTS. Вивчення коду для реалізації програми CTS Dispose тут було дуже проникливим, і підкреслюється порівняння @ SørenBoisen з витоками реєстрації обробника подій.
BitMask777

Коментарі вище відображають стан обговорення, якщо інша відповідь @Bryan Crosby була прийнята.
Джордж Мамаладзе

У документації до 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-us/dotnet/standard/threading/…
Endrju

44

Я не вважав, що жодна з нинішніх відповідей є задовільною. Після дослідження я знайшов цю відповідь у Стівена Туба ( посилання ):

Це залежить. У .NET 4 CTS.Dispose служив двом основним цілям. Якщо доступ до WaitHandle CancellationToken був доступний (таким чином, ліниво виділяючи його), Dispose розпорядиться цією ручкою. Крім того, якщо CTS був створений методом CreateLinkedTokenSource, Dispose від’єднає CTS від маркерів, з якими він був пов'язаний. У .NET 4.5 Dispose має додаткове призначення, тобто якщо CTS використовує Таймер під кришками (наприклад, було викликано CancelAfter), Таймер буде розміщений.

Використовувати CancellationToken.WaitHandle дуже рідко, тому прибирання після нього, як правило, не є великою причиною для використання утилізації. Якщо ви створюєте свої CTS за допомогою CreateLinkedTokenSource, або якщо ви використовуєте функцію таймера CTS, використання Dispose може бути більш ефектним.

Смілива частина, на мою думку, є важливою частиною. Він використовує "більш вражаючий", що залишає його трохи розпливчастим. Я інтерпретую це як сенс виклику Disposeв тих ситуаціях, що потрібно робити, інакше використання Disposeне потрібно.


10
Більш вражаючий означає, що дочірній CTS додається до батьківського. Якщо ви не позбудетесь дитини, витік буде, якщо батько довгоживе. Тому дуже важливо розпоряджатися пов'язаними.
Григорій

26

Я поглянув на ILSpy, CancellationTokenSourceале я можу знайти лише те, m_KernelEventщо є насправді a ManualResetEvent, що клас обгортки для WaitHandleоб'єкта. З цим слід вирішувати належним чином ГК.


7
У мене таке ж відчуття, що GC очистить все це. Я спробую це переконати. Чому в цьому випадку реалізовані Microsoft розпоряджаються? Щоб позбутися зворотних викликів подій і, можливо, уникнути поширення в GC другого покоління. У цьому випадку виклик розпорядження необов’язковий - дзвоніть, якщо можете, якщо не просто ігноруйте. Не найкращим чином я думаю.
Джордж Мамаладзе

4
Я досліджував це питання. CancellationTokenSource збирає сміття. Ви можете допомогти утилізувати це в Ген 1 ГК. Прийнято.
Джордж Мамаладзе

1
Я провів це те саме розслідування самостійно і дійшов того ж висновку: розпоряджайтеся, якщо можете легко, але не переживайте, намагаючись зробити це у рідкісних, але не нечуваних випадках, коли ви надіслали СкасуванняВзяте завантажувачі і не хочуть чекати, коли вони повернуть листівку назад, вказуючи на те, що вони з цим готові. Це відбуватиметься час від часу через характер того, для чого використовується CancellationToken, і це, правда, обіцяю.
Джо Амента

6
Мій вище коментар не стосується пов'язаних джерел токенів; Я не міг довести, що нормально залишати це нерозкритим, і мудрість у цій темі та MSDN дозволяє припустити, що цього не може бути.
Джо Амента

23

Ви завжди повинні розпоряджатися CancellationTokenSource.

Як розпоряджатися, залежить саме від сценарію. Ви пропонуєте кілька різних сценаріїв.

  1. usingпрацює лише тоді, коли ви використовуєте CancellationTokenSourceякусь паралельну роботу, яку ви чекаєте. Якщо це ваш сенаріо, то чудово, це найпростіший метод.

  2. Під час використання завдань використовуйте ContinueWithзавдання, як ви вказали, щоб розпоряджатися CancellationTokenSource.

  3. Для plinq ви можете використовувати, usingоскільки ви працюєте паралельно, але чекаєте, коли всі паралельно працюючі робітники закінчать.

  4. Для користувальницького інтерфейсу можна створити нову CancellationTokenSourceдля кожної операції, що скасовується, яка не прив’язана до одного тригера скасування. Підтримуйте а List<IDisposable>та додайте кожне джерело до списку, видаляючи їх усі, коли ваш компонент розміщений.

  5. Для потоків створіть новий потік, який з'єднує всі робочі потоки та закриває єдине джерело, коли всі робочі потоки закінчені. Див. СкасуванняTokenSource, Коли розпоряджатися?

Завжди є спосіб. IDisposableекземпляри завжди слід утилізувати. Зразки часто не є тому, що вони є або швидкими зразками для показу основного використання, або тому, що додавання в усі аспекти демонструваного класу було б надмірно складним для вибірки. Вибірка - це просто зразок, не обов'язково (або навіть зазвичай) код якості продукції. Не всі зразки прийнятні для копіювання у виробничий код як є.


для пункту 2, будь-яка причина, яку ви не змогли використати awaitу виконанні завдання та розпоряджатися програмою CancellationTokenSource у коді, який з’являється після очікування?
стинь

14
Є застереження. Якщо CTS буде скасовано під час васawait час операції, ви можете відновити її через OperationCanceledException. Ви можете зателефонувати Dispose(). Але якщо все ще запущені операції та використовують відповідні CancellationToken, цей маркер як і раніше повідомляє CanBeCanceledпро trueте, що джерело розміщено. Якщо вони намагаються зареєструвати зворотний виклик скасування, БУМ! , ObjectDisposedException. Досить безпечно зателефонувати Dispose()після успішного завершення операції. Це стає справді складним, коли вам потрібно щось скасувати.
Майк Стробель

8
Неприхильні до причини Майка Стробеля - примушування правила завжди викликати розпорядження може ввести вас у волохаті ситуації при роботі з CTS та Завданнями через їх асинхронність. Натомість це правило повинно бути таким: завжди розпоряджайтеся пов'язаними джерелами токенів.
Søren Boisen

1
Ваше посилання переходить до видаленої відповіді.
Trisped

19

Ця відповідь все ще з’являється в пошуку Google, і я вважаю, що відповідь, що проголосується, не дає повної історії. Переглянувши вихідний код для CancellationTokenSource(CTS) та CancellationToken(CT), я вважаю, що для більшості випадків використання наступна кодова послідовність є нормальною:

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

m_kernelHandleВнутрішнє поле згадувалося вище , є об'єкт синхронізації підтримавши WaitHandleвластивість в обох класах CTS і КТ. Це створюється лише за наявності доступу до цієї власності. Таким чином, якщо ви не використовуєте WaitHandleдля синхронізації потоків старої школи у своєму Taskрозпорядженні викликом, це не матиме ефекту.

Звичайно, якщо ви будете використовувати його , ви повинні робити те , що пропонуються іншими відповідями вище і затримка виклику , Disposeпоки яка - або WaitHandleоперація з використанням ручки не є повною, оскільки, як описано в API документації Windows , для WaitHandle , результати не визначені.


7
У статті MSDN Скасування в керованих потоках зазначено: "Слухачі відстежують значення IsCancellationRequestedвластивості токена шляхом опитування, зворотного дзвінка або ручки очікування." Іншими словами: Ви не (саме той, хто подає запит на асинхронізацію) використовуєте ручку очікування, це може бути слухач (тобто той, хто відповідає на запит). Це означає, що ви як особа, відповідальна за утилізацію, фактично не маєте контролю над тим, використовується ручка очікування чи ні.
herzbube

Згідно з MSDN, зареєстровані зворотні виклики, які мали виняток, призведуть до скасування .Cancel. Ваш код не зателефонує. Відмініть (), якщо це станеться. Зворотні дзвінки повинні бути обережними, щоб цього не робити, але це може статися.
Джозеф Леннокс

11

З давніх пір я запитав це і отримав багато корисних відповідей, але я натрапив на цікаве питання, пов’язане з цим, і подумав, що опублікую його тут як чергову відповідь:

Вам слід зателефонувати CancellationTokenSource.Dispose()лише тоді, коли ви впевнені, що ніхто не збирається намагатися отримати Tokenмайно CTS . В іншому випадку ви повинні НЕ називати, тому що це гонка. Наприклад, дивіться тут:

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

У виправлення цього питання код, який раніше cts.Cancel(); cts.Dispose();було відредаговано, просто робив, cts.Cancel();тому що хтось настільки невдалий, що намагається отримати маркер скасування, щоб дотримуватися його стану скасування після Dispose виклику, на жаль, також потрібно буде обробити ObjectDisposedException- на додаток до OperationCanceledExceptionщо вони планували.

Ще одне ключове зауваження, пов’язане з цим виправленням, зроблено Tratcher: "Утилізація потрібна лише для жетонів, які не будуть скасовані, оскільки скасування робить все те саме очищення". тобто просто робити Cancel()замість того, щоб розпоряджатися - це дуже добре!


1

Я створив безпечний для потоків клас, який прив'язує a CancellationTokenSourceдо a Taskі гарантує, що CancellationTokenSourceзаповіт буде розпоряджатися, коли його асоційоване Taskзавершення. Він використовує блокування, щоб гарантувати, що CancellationTokenSourceзаповіт не буде скасовано під час або після його утилізації. Це відбувається з дотриманням документації , в якій зазначено:

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класу є RunAsyncі Cancel. За замовчуванням одночасні операції не дозволені, це означає, що дзвінкиRunAsync вдруге безшумно скасовується і чекає завершення попередньої операції (якщо вона все ще виконується), перш ніж запустити нову операцію.

Цей клас можна використовувати в будь-яких програмах. Його основне використання, хоча і в програмах інтерфейсу, всередині форм із кнопками для запуску та скасування асинхронної операції або зі списком, що скасовує та перезапускає операцію щоразу, коли вибраний елемент змінюється. Ось приклад першого випадку:

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Метод приймає додатковий в CancellationTokenякості аргументу, який пов'язаний з внутрішньо створеним CancellationTokenSource. Подання цього необов'язкового маркера може бути корисним у сценаріях просування.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.