Ich habe eine thread-sichere Klasse erstellt, die a CancellationTokenSource
an a bindet Task
und garantiert, dass die Klasse CancellationTokenSource
entsorgt wird, wenn die zugehörige Task
abgeschlossen ist. Es verwendet Schlösser, um sicherzustellen, dass das CancellationTokenSource
Produkt während oder nach der Entsorgung nicht gelöscht wird. Dies geschieht zur Einhaltung der Dokumentation , in der Folgendes angegeben ist:
Die Dispose
Methode darf nur verwendet werden, wenn alle anderen Vorgänge für das CancellationTokenSource
Objekt abgeschlossen sind.
Und auch :
Die Dispose
Methode belässt das CancellationTokenSource
in einem unbrauchbaren Zustand.
Hier ist die Klasse:
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;
}
Die primären Methoden der CancelableExecution
Klasse sind die RunAsync
und die Cancel
. Standardmäßig sind gleichzeitige Vorgänge nicht zulässig, dh das AufrufenRunAsync
ein zweites stillschweigend abgebrochen wird und auf den Abschluss des vorherigen Vorgangs (sofern dieser noch ausgeführt wird) gewartet wird, bevor der neue Vorgang gestartet wird.
Diese Klasse kann in Anwendungen jeglicher Art verwendet werden. Die Hauptverwendung erfolgt jedoch in UI-Anwendungen, in Formularen mit Schaltflächen zum Starten und Abbrechen eines asynchronen Vorgangs oder in einem Listenfeld, das einen Vorgang jedes Mal abbricht und neu startet, wenn das ausgewählte Element geändert wird. Hier ist ein Beispiel für den ersten Fall:
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();
}
Die RunAsync
Methode akzeptiert ein Extra CancellationToken
als Argument, das mit dem intern erstellten verknüpft ist CancellationTokenSource
. Das Bereitstellen dieses optionalen Tokens kann in Fortschrittsszenarien hilfreich sein.