Я создал потокобезопасный класс, который связывает 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
. Поставка этого дополнительного токена может быть полезна в сценариях продвижения.