解決したいこと
クライアントアプリで非同期タスクをキャンセルできるようにするとき次のようなコードを実装しますが、CancellationTokenSource に関する実装をもっと簡単にしたいと考えています。
- CancellationTokenSource の解放漏れを防ぎたい。
- 単純なタスクのキャンセルは、CancellationTokenSource を過度に意識することなく実装できるようにしたい。
- 対象のタスクの多重実行を認める場合はタスクとキャンセルトークンの組み合わせをコレクションで管理するような実装が必要になるが、これを簡単に実装したい。
// 実行中のタスクをキャンセルするためのキャンセルトークン
private CancellationTokenSource? m_CurrentCancellation;
/// <summary>
/// 実行します。
/// </summary>
/// <returns></returns>
private async Task RunAsync()
{
using CancellationTokenSource cancellation = new CancellationTokenSource();
m_CurrentCancellation = cancellation;
try
{
await ExecuteAsync(cancellation.Token).ConfigureAwait(false);
}
finally
{
m_CurrentCancellation = null;
}
}
/// <summary>
/// 実行中のタスクをキャンセルします。
/// </summary>
private void Cancel()
{
m_CurrentCancellation?.Cancel();
}
/// <summary>
/// 実行対象の非同期処理
/// </summary>
/// <param name="cancellation">キャンセルトークン</param>
/// <returns></returns>
private Task ExecuteAsync(CancellationToken cancellation = default)
{
return Task.Delay(1000);
}
キャンセル処理をカプセル化する
CancellationTokenSource に関する実装をカプセル化したクラスを定義すると、前述のサンプルコードは次のように書き換えることができそうです。CancellationTokenSource の代わりに Guid を用いてキャンセルしたいタスクに対応するキャンセルトークンを指定することにしました。
public class TaskManager
{
// 実行中のタスクをキャンセルするためのキャンセルトークン
private readonly ConcurrentDictionary<Guid, CancellationTokenSource> m_CurrentCancellations
= new ConcurrentDictionary<Guid, CancellationTokenSource>();
/// <summary>
/// 指定された非同期メソッドを実行します。
/// </summary>
/// <param name="taskId">タスクを一意に識別するID</param>
/// <param name="function">実行する非同期メソッド</param>
/// <returns></returns>
private async Task RunWithCancellation(Guid taskId, Func<CancellationToken, Task> function)
{
using CancellationTokenSource cancellation = new CancellationTokenSource();
try
{
if (!m_CurrentCancellations.TryAdd(taskId, cancellation))
{
throw new ArgumentException("指定されたタスクIDは既に登録されています。");
}
await ExecuteAsync(cancellation.Token).ConfigureAwait(false);
}
finally
{
m_CurrentCancellations.Remove(taskId, out _);
}
}
/// <summary>
/// 指定されたタスクをキャンセルします。
/// </summary>
/// <param name="taskId">タスクを一意に識別するID</param>
public void Cancel(Guid taskId)
{
if (m_CurrentCancellations.TryGetValue(taskId, out var cancellation)
{
cancellation.Cancel();
}
}
}
キャンセルトークンの代わりにタスクIDを保持しておかなくてはならない煩わしさは残りますが、CancellationTokenSource をカプセル化することができました。TaskManager クラスに IDisposable インターフェースを実装し、実行中のタスクをキャンセル&トークンを解放するようにすれば解放漏れを防ぎやすくなると思います。
private readonly TaskManager m_Manager = new TaskManager();
// 実行中のタスクを表すID
private Guid? m_CurrentTaskId;
/// <summary>
/// 実行します。
/// </summary>
/// <returns></returns>
private async Task RunAsync()
{
m_CurrentTaskId = Guid.NewGuid();
try
{
await m_Manager.RunWithCancellation(currentTaskId, ExecuteAsync).ConfigureAwait(false);
}
finally
{
m_CurrentTaskId = null;
}
}
/// <summary>
/// 実行中のタスクをキャンセルします。
/// </summary>
private void Cancel()
{
if (m_CurrentTaskId.HasValue)
{
m_Manager.Cancel(m_CurrentTaskId.Value);
}
}
/// <summary>
/// 実行対象の非同期処理
/// </summary>
/// <param name="cancellation">キャンセルトークン</param>
/// <returns></returns>
private Task ExecuteAsync(CancellationToken cancellation = default)
{
return Task.Delay(1000);
}
実装したクラス
リスナーによる完了通知機能を加えた TaskObserver クラスとして実装しました。
private readonly TaskObserver TaskObserver = new TaskObserver();
private IDisposable? TaskListenerUnregister;
/// <summary>
/// TaskObserver に関する初期処理
/// </summary>
private void InitializeTaskObserver()
{
TaskListenerUnregister = TaskObserver.RegisterListener(OnStart, OnCompleted, OnFailed);
}
/// <summary>
/// タスクが開始されるときの処理を行います。
/// </summary>
/// <param name="taskId"></param>
private void OnStart(TaskID taskId)
{
Debug.WriteLine($"Start the task. ID:{taskId}");
}
/// <summary>
/// タスクが完了したときの処理を行います。
/// </summary>
/// <param name="taskId"></param>
private void OnCompleted(TaskID taskId)
{
Debug.WriteLine($"The task is complete. ID:{taskId}");
}
/// <summary>
/// タスクが失敗したときの処理を行います。
/// </summary>
/// <param name="taskId"></param>
/// <param name="exception"></param>
private void OnFailed(TaskID taskId, Exception? exception)
{
Debug.WriteLine($"The task is failed. {exception?.Message} ID:{taskId}");
}
/// <summary>
/// 明示的に生成されたキャンセルトークンを使用して非同期処理を実行します。
/// </summary>
/// <returns></returns>
private Task ExecuteActionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation)
{
return TaskObserver.Run(
taskId
, ExecuteActionAsync
, cancellation
, disposableCancellation: false
);
}
/// <summary>
/// 暗黙的に生成されるキャンセルトークンを使用して非同期処理を実行します。
/// </summary>
/// <returns></returns>
private Task ExecuteActionWithImplicitCancellationToken(Guid taskId)
{
return TaskObserver.RunWithCancellation(
taskId
, ExecuteActionAsync
);
}
/// <summary>
/// 明示的に生成されたキャンセルトークンを使用して非同期処理を実行します。
/// </summary>
/// <returns></returns>
private Task<int> ExecuteFunctionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation)
{
return TaskObserver.Run(
taskId
, ExecuteFunctionAsync
, cancellation
, disposableCancellation: false
);
}
/// <summary>
/// 暗黙的に生成されるキャンセルトークンを使用して非同期処理を実行します。
/// </summary>
/// <returns></returns>
private Task<int> ExecuteFunctionWithImplicitCancellationToken(Guid taskId)
{
return TaskObserver.RunWithCancellation(
taskId
, ExecuteFunctionAsync
);
}
/// <summary>
/// 明示的に生成されたキャンセルトークンによって制御されるタスクを監視します。
/// </summary>
/// <param name="taskId"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
private Task ObserveActionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation)
{
return TaskObserver.Observe(
taskId
, ExecuteActionAsync(cancellation.Token)
, cancellation
, disposableCancellation: false
);
}
/// <summary>
/// 暗黙的に生成されるキャンセルトークンによって制御されるタスクを監視します。
/// </summary>
/// <param name="taskId"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
private Task ObserveActionWithImplicitCancellationToken(Guid taskId)
{
var cancellation = new CancellationTokenSource();
return TaskObserver.Observe(
taskId
, ExecuteActionAsync(cancellation.Token)
, cancellation
, disposableCancellation: true
);
}
/// <summary>
/// 明示的に生成されたキャンセルトークンによって制御されるタスクを監視します。
/// </summary>
/// <param name="taskId"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
private Task<int> ObserveFunctionWithCancellationToken(Guid taskId, CancellationTokenSource cancellation)
{
return TaskObserver.Observe(
taskId
, ExecuteFunctionAsync(cancellation.Token)
, cancellation
, disposableCancellation: false
);
}
/// <summary>
/// 暗黙的に生成されるキャンセルトークンによって制御されるタスクを監視します。
/// </summary>
/// <param name="taskId"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
private Task<int> ObserveFunctionWithImplicitCancellationToken(Guid taskId)
{
var cancellation = new CancellationTokenSource();
return TaskObserver.Observe(
taskId
, ExecuteFunctionAsync(cancellation.Token)
, cancellation
, disposableCancellation: true
);
}
/// <summary>
/// 指定されたタスクをキャンセルします。
/// </summary>
/// <param name="taskId"></param>
private void Cancel(Guid taskId)
{
TaskObserver.RequestCancel(taskId);
}
/// <summary>
/// 全てのタスクをキャンセルします。
/// </summary>
private void CancelAll()
{
TaskObserver.RequestCancelAll();
}
/// <summary>
/// 指定されたタスクが実行中かどうかを取得します。
/// </summary>
/// <param name="taskId"></param>
/// <returns></returns>
private bool IsRunning(Guid taskId)
{
return TaskObserver.IsRunning(taskId);
}
/// <summary>
/// 非同期処理のサンプル
/// </summary>
/// <param name="cancellation"></param>
/// <returns></returns>
private Task ExecuteActionAsync(CancellationToken cancellation = default)
{
return Task.Delay(1000);
}
/// <summary>
/// 戻り値を返す非同期処理のサンプル
/// </summary>
/// <param name="cancellation"></param>
/// <returns></returns>
private Task<int> ExecuteFunctionAsync(CancellationToken cancellation = default)
{
return Task.Delay(1000).ContinueWith(t => 10);
}
まとめ
CancellationTokenSource に関連する実装量を減らすことができたと思います。
Task, Task<TResult> から fluently に記述できたりなど、もっとスマートに実装できる方法があるとよいのですが。