Когда утилизировать CancellationTokenSource?

163

Класс CancellationTokenSourceодноразовый. Быстрый просмотр Reflector доказывает использование KernelEvent(весьма вероятного) неуправляемого ресурса. Так CancellationTokenSourceкак не имеет финализатора, если мы не распорядимся им, сборщик мусора не сделает этого.

С другой стороны, если вы посмотрите на примеры, перечисленные в статье 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в моей реализации.

Gruzilkin
источник
14
Это важное упущение в текущем принятом ответе Брайана Кросби - если вы создаете связанный CTS, вы рискуете утечки памяти. Сценарий очень похож на обработчики событий, которые никогда не регистрируются.
Сорен Бойсен
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, поэтому очистка после него, как правило, не является веской причиной для использования Dispose. Однако, если вы создаете свой CTS с помощью CreateLinkedTokenSource, или если вы используете функцию таймера CTS, использование Dispose может быть более эффективным.

Смелая часть, я думаю, является важной частью. Он использует «более эффектный», что делает его немного расплывчатым. Я интерпретирую это как значение вызова Disposeв таких ситуациях, иначе использование Disposeне нужно.

Джесси Гуд
источник
10
Более эффективный означает, что дочерний CTS добавляется к родительскому. Если вы не распорядитесь ребенком, будет утечка, если родитель долгоживущий. Поэтому очень важно избавиться от связанных.
Григорий
26

Я взглянул на ILSpy, CancellationTokenSourceно могу найти только то, m_KernelEventчто на самом деле ManualResetEventявляется классом-оболочкой для WaitHandleобъекта. Это должно быть правильно обработано GC.

Брайан Кросби
источник
7
У меня такое же чувство, что GC очистит все это. Я постараюсь это проверить. Почему Microsoft реализовала утилизацию в этом случае? Вероятно, чтобы избавиться от обратных вызовов событий и избежать распространения на GC второго поколения. В этом случае вызывать Dispose необязательно - вызывайте его, если можете, если не просто игнорируйте. Не самая лучшая манера, я думаю.
Георгий Мамаладзе
4
Я исследовал эту проблему. CancellationTokenSource получает сборщик мусора. Вы могли бы помочь с распоряжением сделать это в GEN 1 GC. Принято.
Георгий Мамаладзе
1
Я провел это же расследование самостоятельно и пришел к тому же выводу: распоряжайтесь, если можете, но не беспокойтесь, пытаясь сделать это в тех редких, но не неслыханных случаях, когда вы отправляете CancellationToken в и не хотят ждать, пока они напишут открытку с сообщением, что с этим покончено. Это будет происходить время от времени из-за характера того, для чего используется CancellationToken, и это действительно хорошо, я обещаю.
Джо Амента
6
Мой комментарий выше не относится к связанным источникам токенов; Я не мог доказать, что это нормально, чтобы оставить эти нераспределенные, и мудрость в этой теме и MSDN предполагает, что это не может быть.
Джо Амента
23

Вы должны всегда распоряжаться CancellationTokenSource.

Как его распорядиться, зависит именно от сценария. Вы предлагаете несколько разных сценариев.

  1. usingработает только тогда, когда вы используете CancellationTokenSourceожидаемую параллельную работу. Если это ваш сенарио, то отлично, это самый простой метод.

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

  3. Вы можете использовать plinq, usingпоскольку вы запускаете его параллельно, но ожидаете завершения работы всех параллельно работающих рабочих.

  4. Для пользовательского интерфейса вы можете создать новую CancellationTokenSourceдля каждой отменяемой операции, которая не привязана к одному триггеру отмены. Сохраните a List<IDisposable>и добавьте каждый источник в список, утилизируя все из них, когда ваш компонент будет удален.

  5. Для потоков создайте новый поток, который объединит все рабочие потоки и закроет один источник после завершения всех рабочих потоков. См. CancellationTokenSource, когда утилизировать?

Там всегда есть способ. IDisposableэкземпляры всегда должны быть утилизированы. Сэмплы часто этого не делают, потому что они либо быстрые сэмплы, чтобы показать использование ядра, либо потому, что добавление во всех аспектах демонстрируемого класса было бы слишком сложным для сэмпла. Образец - это просто образец, а не обязательно (или даже обычно) производственный код качества. Не все образцы могут быть скопированы в производственный код как есть.

Сэмюэл Нефф
источник
по пункту 2, по какой причине вы не могли использовать awaitзадачу и располагать CancellationTokenSource в коде, который идет после ожидания?
Стийн
14
Есть предостережения. Если CTS отменяется во время вашей awaitоперации, вы можете возобновить работу из-за OperationCanceledException. Вы могли бы тогда позвонить Dispose(). Но если есть операции, которые все еще выполняются и используют соответствующие CancellationToken, этот токен все еще сообщает о том, CanBeCanceledчто он находится, trueдаже если источник утилизирован. Если они пытаются зарегистрировать отмену обратного вызова, BOOM! , ObjectDisposedException. Достаточно безопасно позвонить Dispose()после успешного завершения операции. Это действительно сложно, когда вам нужно что-то отменить.
Майк Штробель
8
Отказался по причинам, указанным Майком Штробелем - принуждение правила всегда вызывать Dispose может привести к неприятным ситуациям при работе с CTS и Task из-за их асинхронного характера. Вместо этого следует придерживаться правила: всегда располагайте связанными источниками токенов.
Сорен Бойсен
1
Ваша ссылка идет на удаленный ответ.
Trisped
19

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

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

m_kernelHandleВнутреннее поле упоминалось выше , является объект синхронизации поддержав WaitHandleсвойство в обоих классах CTS и КТ. Он создается только при доступе к этому свойству. Таким образом, если вы не используете WaitHandleкакую-либо синхронизацию потоков старой школы в вашем Taskвызове dispose, это не будет иметь никакого эффекта.

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

jlyonsmith
источник
7
В статье MSDN « Отмена в управляемых потоках» говорится: «Слушатели отслеживают значение IsCancellationRequestedсвойства токена с помощью опроса, обратного вызова или дескриптора ожидания». Другими словами: это может быть не вы (то есть тот, кто делает асинхронный запрос), который использует дескриптор ожидания, это может быть слушатель (то есть тот, кто отвечает на запрос). Это означает, что вы, как ответственный за утилизацию, фактически не можете контролировать, используется ли ручка ожидания или нет.
herzbube
Согласно MSDN, зарегистрированные обратные вызовы, которые исключили, вызовут .Cancel, чтобы бросить. Ваш код не будет вызывать .Dispose (), если это произойдет. Обратные вызовы должны быть осторожны, чтобы не делать этого, но это может произойти.
Джозеф Леннокс
11

Прошло много времени с тех пор, как я спрашивал об этом и получал много полезных ответов, но я наткнулся на интересную проблему, связанную с этим, и подумал, что опубликую это здесь как еще один ответ:

Вы должны звонить CancellationTokenSource.Dispose()только тогда, когда вы уверены, что никто не собирается пытаться получить собственность CTS Token. В противном случае вы не должны называть это, потому что это гонка. Например, смотрите здесь:

https://github.com/aspnet/AspNetKatana/issues/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. Поставка этого дополнительного токена может быть полезна в сценариях продвижения.

Теодор Зулиас
источник