J'ai créé une classe thread-safe qui lie a CancellationTokenSource
à a Task
et garantit que le CancellationTokenSource
sera supprimé une fois son associé Task
terminé. Il utilise des verrous pour garantir que le CancellationTokenSource
ne sera pas annulé pendant ou après sa mise au rebut. Cela se produit pour la conformité à la documentation , qui stipule:
La Dispose
méthode ne doit être utilisée que lorsque toutes les autres opérations sur l' CancellationTokenSource
objet sont terminées.
Et aussi :
La Dispose
méthode laisse le CancellationTokenSource
dans un état inutilisable.
Voici la classe:
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;
}
Les principales méthodes de la CancelableExecution
classe sont les RunAsync
et les Cancel
. Par défaut, les opérations simultanées ne sont pas autorisées, ce qui signifie que l'appelRunAsync
deuxième annulera silencieusement et attendra la fin de l'opération précédente (si elle est toujours en cours), avant de démarrer la nouvelle opération.
Cette classe peut être utilisée dans des applications de tout type. Son utilisation principale est cependant dans les applications d'interface utilisateur, dans des formulaires avec des boutons pour démarrer et annuler une opération asynchrone, ou avec une zone de liste qui annule et redémarre une opération à chaque fois que son élément sélectionné est modifié. Voici un exemple du premier cas:
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();
}
La RunAsync
méthode accepte un extra CancellationToken
comme argument, qui est lié au créé en interne CancellationTokenSource
. Fournir ce jeton facultatif peut être utile dans les scénarios avancés.