¿Cuándo deshacerse de CancellationTokenSource?

163

La clase CancellationTokenSourcees desechable. Un vistazo rápido en Reflector demuestra el uso de KernelEventun recurso no administrado (muy probable). Como CancellationTokenSourceno tiene finalizador, si no lo desechamos, el GC no lo hará.

Por otro lado, si observa las muestras enumeradas en el artículo Cancelación de MSDN en Subprocesos administrados , solo un fragmento de código elimina el token.

¿Cuál es la forma correcta de deshacerse de él en código?

  1. No puede ajustar el código con el que comienza su tarea paralela usingsi no lo espera. Y tiene sentido tener cancelación solo si no espera.
  2. Por supuesto, puede agregar una ContinueWithtarea con una Disposellamada, pero ¿es ese el camino a seguir?
  3. ¿Qué pasa con las consultas PLINQ cancelables, que no se sincronizan de nuevo, sino que simplemente hacen algo al final? Vamos a decir .ForAll(x => Console.Write(x))?
  4. ¿Es reutilizable? ¿Se puede usar el mismo token para varias llamadas y luego desecharlo junto con el componente host, digamos control UI?

Debido a que no tiene algo como un Resetmétodo de limpieza IsCancelRequestedy Tokencampo, supongo que no es reutilizable, por lo tanto, cada vez que inicie una tarea (o una consulta PLINQ), debe crear una nueva. ¿Es verdad? En caso afirmativo, mi pregunta es ¿cuál es la estrategia correcta y recomendada para tratar Disposeen esos CancellationTokenSourcecasos?

George Mamaladze
fuente

Respuestas:

82

Hablando sobre si es realmente necesario llamar a Dispose on CancellationTokenSource... Tuve una pérdida de memoria en mi proyecto y resultó que ese CancellationTokenSourceera el problema.

Mi proyecto tiene un servicio, que lee constantemente la base de datos y dispara diferentes tareas, y estaba pasando tokens de cancelación vinculados a mis trabajadores, por lo que incluso después de que terminaron de procesar los datos, los tokens de cancelación no se eliminaron, lo que causó una pérdida de memoria.

La cancelación de MSDN en subprocesos administrados lo establece claramente:

Tenga en cuenta que debe llamar Disposeal origen del token vinculado cuando haya terminado con él. Para un ejemplo más completo, vea Cómo: escuchar las solicitudes de cancelación múltiple .

Utilicé ContinueWithen mi implementación.

Gruzilkin
fuente
14
Esta es una omisión importante en la respuesta actual aceptada por Bryan Crosby: si crea un CTS vinculado , corre el riesgo de pérdidas de memoria. El escenario es muy similar a los controladores de eventos que nunca están sin registrar.
Søren Boisen
55
Tuve una fuga debido a este mismo problema. Usando un generador de perfiles pude ver registros de devolución de llamada que contienen referencias a las instancias de CTS vinculadas. Examinar el código para la implementación de CTS Dispose aquí fue muy perspicaz y subraya la comparación de @ SørenBoisen con las fugas de registro del controlador de eventos.
BitMask777
Los comentarios anteriores reflejan el estado de discusión. La otra respuesta de @Bryan Crosby fue aceptada.
George Mamaladze
La documentación en 2020 dice claramente: 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

No pensé que ninguna de las respuestas actuales fuera satisfactoria. Después de investigar encontré esta respuesta de Stephen Toub ( referencia ):

Depende. En .NET 4, CTS.Dispose tenía dos propósitos principales. Si se accedió a la WaitHandle de CancellationToken (por lo tanto, la asignó perezosamente), Dispose eliminará ese controlador. Además, si el CTS se creó mediante el método CreateLinkedTokenSource, Dispose desvinculará el CTS de los tokens a los que estaba vinculado. En .NET 4.5, Dispose tiene un propósito adicional, que es si el CTS usa un Timer debajo de las cubiertas (por ejemplo, se llamó a CancelAfter), el Timer será descartado.

Es muy raro que se use CancellationToken.WaitHandle, por lo que la limpieza después de eso generalmente no es una buena razón para usar Dispose. Sin embargo, si está creando su CTS con CreateLinkedTokenSource, o si está utilizando la funcionalidad del temporizador de CTS, puede ser más impactante usar Dispose.

La parte audaz creo que es la parte importante. Él usa "más impactante", lo que lo deja un poco vago. Lo estoy interpretando como que Disposese debe hacer un llamado en esas situaciones, de lo contrario Disposeno es necesario usarlo .

Jesse Good
fuente
10
Más impactante significa que el CTS secundario se agrega al padre uno. Si no dispone al niño, habrá una fuga si el padre es longevo. Por lo tanto, es fundamental disponer de los vinculados.
Grigory
26

Eché un vistazo en ILSpy para el, CancellationTokenSourcepero solo puedo encontrar m_KernelEventcuál es realmente una ManualResetEvent, que es una clase de contenedor para un WaitHandleobjeto. Esto debe ser manejado adecuadamente por el GC.

Bryan Crosby
fuente
77
Tengo la misma sensación de que GC limpiará todo eso. Trataré de verificar eso. ¿Por qué Microsoft implementó disponer en este caso? Para deshacerse de las devoluciones de llamadas de eventos y evitar la propagación a GC de segunda generación probablemente. En este caso, llamar a Dispose es opcional: llámelo si puede, si no, simplemente ignórelo. No es la mejor manera, creo.
George Mamaladze
44
He investigado este problema. CancellationTokenSource obtiene la basura recolectada. Puede ayudar a deshacerse de hacerlo en GEN 1 GC. Aceptado.
George Mamaladze
1
Hice esta misma investigación de forma independiente y llegué a la misma conclusión: elimine si puede hacerlo fácilmente, pero no se preocupe por intentar hacerlo en los casos raros pero no desconocidos en los que ha enviado un CancellationToken a los boondocks y no quiero esperar a que escriban una tarjeta postal diciéndole que ya terminaron. Esto va a suceder de vez en cuando debido a la naturaleza de para qué se utiliza CancellationToken, y está realmente bien, lo prometo.
Joe Amenta
66
Mi comentario anterior no se aplica a las fuentes de token vinculadas; No pude demostrar que está bien dejarlos sin discutir, y la sabiduría en este hilo y MSDN sugiere que podría no ser así.
Joe Amenta
23

Siempre debes disponer CancellationTokenSource.

Cómo desecharlo depende exactamente del escenario. Propones varios escenarios diferentes.

  1. usingsolo funciona cuando está utilizando CancellationTokenSourcealgún trabajo paralelo que está esperando. Si ese es tu senario, entonces genial, es el método más fácil.

  2. Cuando use tareas, use una ContinueWithtarea como indicó para deshacerse de ella CancellationTokenSource.

  3. Para plinq puedes usarlo usingya que lo estás ejecutando en paralelo pero esperando a que todos los trabajadores de ejecución paralela terminen.

  4. Para la IU, puede crear una nueva CancellationTokenSourcepara cada operación cancelable que no esté vinculada a un solo desencadenador de cancelación. Mantenga una List<IDisposable>y agregue cada fuente a la lista, eliminándolas todas cuando se elimine su componente.

  5. Para los subprocesos, cree un nuevo subproceso que se una a todos los subprocesos de trabajo y cierre la fuente única cuando finalicen todos los subprocesos de trabajo. Ver CancellationTokenSource, ¿Cuándo desechar?

Siempre hay una manera. IDisposableLas instancias siempre deben desecharse. Las muestras a menudo no lo hacen porque son muestras rápidas para mostrar el uso principal o porque agregar todos los aspectos de la clase que se está demostrando sería demasiado complejo para una muestra. La muestra es solo una muestra, no necesariamente (o incluso generalmente) el código de calidad de producción. No todas las muestras son aceptables para copiarse en el código de producción tal cual.

Samuel Neff
fuente
para el punto 2, ¿hay alguna razón por la que no pueda usar awaiten la tarea y desechar el CancellationTokenSource en el código que viene después de la espera?
stijn
14
Hay advertencias. Si el CTS se cancela mientras realiza awaituna operación, puede reanudarlo debido a un OperationCanceledException. Entonces puedes llamar Dispose(). Pero si todavía hay operaciones que se ejecutan y usan el correspondiente CancellationToken, ese token aún se informa CanBeCanceledcomo trueaunque la fuente esté eliminada. Si intentan registrar una devolución de llamada de cancelación, BOOM! , ObjectDisposedException. Es lo suficientemente seguro para llamar Dispose()después de completar con éxito las operaciones. Se vuelve realmente complicado cuando realmente necesitas cancelar algo.
Mike Strobel
8
Votado por las razones dadas por Mike Strobel: forzar una regla para llamar siempre a Dispose puede llevarlo a situaciones difíciles cuando se trata de CTS y Task debido a su naturaleza asincrónica. La regla debería ser: eliminar siempre las fuentes de token vinculadas .
Søren Boisen
1
Su enlace va a una respuesta eliminada.
Trisped
19

Esta respuesta sigue apareciendo en las búsquedas de Google, y creo que la respuesta votada no da la historia completa. Después de revisar el código fuente de CancellationTokenSource(CTS) y CancellationToken(CT), creo que para la mayoría de los casos de uso, la siguiente secuencia de código está bien:

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

El m_kernelHandlecampo interno mencionado anteriormente es el objeto de sincronización que respalda la WaitHandlepropiedad en las clases CTS y CT. Solo se crea una instancia si accede a esa propiedad. Por lo tanto, a menos que esté utilizando WaitHandlepara algunos sincronización de hilos de la vieja escuela en su Taskdisponer de llamada no tendrá ningún efecto.

Por supuesto, si lo está utilizando, debe hacer lo que sugieren las otras respuestas anteriores y retrasar la llamada Disposehasta que WaitHandlese complete cualquier operación que use el identificador, porque, como se describe en la documentación de la API de Windows para WaitHandle , los resultados no están definidos.

jlyonsmith
fuente
77
El artículo de Cancelación de MSDN en subprocesos administrados establece: "Los oyentes supervisan el valor de la IsCancellationRequestedpropiedad del token mediante sondeo, devolución de llamada o espera". En otras palabras: puede que no sea usted (es decir, el que realiza la solicitud asíncrona) quien utiliza el identificador de espera, puede ser el oyente (es decir, el que responde la solicitud). Lo que significa que usted, como el responsable de la eliminación, no tiene control sobre si se utiliza o no el tirador de espera.
herzbube
Según MSDN, las devoluciones de llamada registradas que hayan hecho una excepción harán que se cancele .Cancelar. Su código no llamará a .Dispose () si esto sucede. Las devoluciones de llamada deben tener cuidado de no hacer esto, pero puede suceder.
Joseph Lennox
11

Ha pasado mucho tiempo desde que pregunté esto y obtuve muchas respuestas útiles, pero me encontré con un problema interesante relacionado con esto y pensé en publicarlo aquí como otra respuesta:

Debe llamar CancellationTokenSource.Dispose()solo cuando esté seguro de que nadie intentará obtener la Tokenpropiedad del CTS . De lo contrario, usted debe no la llamas, porque es una carrera. Por ejemplo, mira aquí:

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

En la solución para este problema, el código que lo hizo anteriormente cts.Cancel(); cts.Dispose();fue editado solo cts.Cancel();porque cualquiera que tenga la mala suerte de intentar obtener el token de cancelación para observar su estado de cancelación después de Dispose haber sido llamado, desafortunadamente también tendrá que manejarlo ObjectDisposedException, además de OperationCanceledExceptionque estaban planeando

Tratcher hace otra observación clave relacionada con esta solución: "La eliminación solo es necesaria para los tokens que no se cancelarán, ya que la cancelación hace la misma limpieza". es decir, ¡hacer en Cancel()lugar de deshacerse es bastante bueno!

Tim Lovell-Smith
fuente
1

Hice una clase segura para subprocesos que une a CancellationTokenSourcea Task, y garantiza que CancellationTokenSourcese eliminará cuando se Taskcomplete su asociado . Utiliza cerraduras para garantizar que CancellationTokenSourceno se cancele durante o después de que se haya eliminado. Esto sucede para cumplir con la documentación , que establece:

El Disposemétodo solo debe usarse cuando se CancellationTokenSourcehayan completado todas las demás operaciones en el objeto.

Y también :

El Disposemétodo deja el CancellationTokenSourceen un estado inutilizable.

Aquí está la clase:

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

Los métodos principales de la CancelableExecutionclase son el RunAsyncy el Cancel. Por defecto, las operaciones concurrentes no están permitidas, lo que significa que llamarRunAsync por segunda vez se cancelará silenciosamente y esperará la finalización de la operación anterior (si aún se está ejecutando), antes de comenzar la nueva operación.

Esta clase se puede usar en aplicaciones de cualquier tipo. Sin embargo, su uso principal es en aplicaciones de IU, dentro de formularios con botones para iniciar y cancelar una operación asincrónica, o con un cuadro de lista que cancela y reinicia una operación cada vez que se cambia su elemento seleccionado. Aquí hay un ejemplo del primer caso:

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

El RunAsyncmétodo acepta un extra CancellationTokencomo argumento, que está vinculado a lo creado internamente CancellationTokenSource. El suministro de este token opcional puede ser útil en escenarios avanzados.

Theodor Zoulias
fuente