Hice una clase segura para subprocesos que une a CancellationTokenSource
a Task
, y garantiza que CancellationTokenSource
se eliminará cuando se Task
complete su asociado . Utiliza cerraduras para garantizar que CancellationTokenSource
no se cancele durante o después de que se haya eliminado. Esto sucede para cumplir con la documentación , que establece:
El Dispose
método solo debe usarse cuando se CancellationTokenSource
hayan completado todas las demás operaciones en el objeto.
Y también :
El Dispose
método deja el CancellationTokenSource
en 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 CancelableExecution
clase son el RunAsync
y 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 RunAsync
método acepta un extra CancellationToken
como argumento, que está vinculado a lo creado internamente CancellationTokenSource
. El suministro de este token opcional puede ser útil en escenarios avanzados.