Saya membuat kelas thread-safe yang mengikat a CancellationTokenSource
ke Task
, dan menjamin bahwa CancellationTokenSource
akan dibuang ketika terkait Task
selesai. Menggunakan kunci untuk memastikan bahwa CancellationTokenSource
tidak akan dibatalkan selama atau setelah dibuang. Ini terjadi untuk kepatuhan dengan dokumentasi , yang menyatakan:
The Dispose
Metode hanya harus digunakan ketika semua operasi lain pada CancellationTokenSource
objek telah selesai.
Dan juga :
The Dispose
Metode meninggalkan CancellationTokenSource
dalam keadaan tidak dapat digunakan.
Inilah kelasnya:
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;
}
Metode utama CancelableExecution
kelas adalah RunAsync
dan Cancel
. Secara default, operasi konkuren tidak diperbolehkan, artinya memanggil RunAsync
kedua kalinya secara diam-diam akan membatalkan dan menunggu penyelesaian operasi sebelumnya (jika masih berjalan), sebelum memulai operasi baru.
Kelas ini dapat digunakan dalam aplikasi apa pun. Penggunaan utamanya adalah dalam aplikasi UI, di dalam formulir dengan tombol untuk memulai dan membatalkan operasi asinkron, atau dengan kotak daftar yang membatalkan dan memulai kembali operasi setiap kali item yang dipilih diubah. Ini adalah contoh dari kasus pertama:
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();
}
The RunAsync
Metode menerima tambahan CancellationToken
sebagai argumen, yang terkait dengan dibuat secara internal CancellationTokenSource
. Memasok token opsional ini mungkin berguna dalam skenario tingkat lanjut.