私たちのアプリケーションは、TPL を使用して (潜在的に) 長時間実行される作業単位をシリアル化します。作業 (タスク) の作成はユーザー主導であり、いつでもキャンセルできます。レスポンシブなユーザー インターフェイスを実現するために、現在の作業が不要になった場合は、これまで行っていたことを破棄して、すぐに別のタスクを開始したいと考えています。
タスクは次のようにキューに入れられます。
private Task workQueue;
private void DoWorkAsync
(Action<WorkCompletedEventArgs> callback, CancellationToken token)
{
if (workQueue == null)
{
workQueue = Task.Factory.StartWork
(() => DoWork(callback, token), token);
}
else
{
workQueue.ContinueWork(t => DoWork(callback, token), token);
}
}
このDoWork
メソッドには実行時間の長い呼び出しが含まれているためtoken.IsCancellationRequested
、キャンセルが検出された場合はステータスを常にチェックして救済するほど単純ではありません。長時間実行される作業は、タスクがキャンセルされた場合でも、終了するまでタスクの継続をブロックします。
この問題を回避するための 2 つのサンプル メソッドを考え出しましたが、どちらも適切であるとは確信していません。簡単なコンソール アプリケーションを作成して、それらがどのように機能するかを示しました。
注意すべき重要な点は、元のタスクが完了する前に継続が開始されることです。
試行 #1: 内部タスク
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
var token = cts.Token;
token.Register(() => Console.WriteLine("Token cancelled"));
// Initial work
var t = Task.Factory.StartNew(() =>
{
Console.WriteLine("Doing work");
// Wrap the long running work in a task, and then wait for it to complete
// or the token to be cancelled.
var innerT = Task.Factory.StartNew(() => Thread.Sleep(3000), token);
innerT.Wait(token);
token.ThrowIfCancellationRequested();
Console.WriteLine("Completed.");
}
, token);
// Second chunk of work which, in the real world, would be identical to the
// first chunk of work.
t.ContinueWith((lastTask) =>
{
Console.WriteLine("Continuation started");
});
// Give the user 3s to cancel the first batch of work
Console.ReadKey();
if (t.Status == TaskStatus.Running)
{
Console.WriteLine("Cancel requested");
cts.Cancel();
Console.ReadKey();
}
}
これは機能しますが、「innerT」タスクは私にとって非常に扱いにくいと感じています。また、この方法で作業をキューに入れるコードのすべての部分をリファクタリングしなければならないという欠点もあります。これは、実行時間の長いすべての呼び出しを新しいタスクにまとめる必要があるためです。
試行 #2: TaskCompletionSource の調整
static void Main(string[] args)
{ var tcs = new TaskCompletionSource<object>();
//Wire up the token's cancellation to trigger the TaskCompletionSource's cancellation
CancellationTokenSource cts = new CancellationTokenSource();
var token = cts.Token;
token.Register(() =>
{ Console.WriteLine("Token cancelled");
tcs.SetCanceled();
});
var innerT = Task.Factory.StartNew(() =>
{
Console.WriteLine("Doing work");
Thread.Sleep(3000);
Console.WriteLine("Completed.");
// When the work has complete, set the TaskCompletionSource so that the
// continuation will fire.
tcs.SetResult(null);
});
// Second chunk of work which, in the real world, would be identical to the
// first chunk of work.
// Note that we continue when the TaskCompletionSource's task finishes,
// not the above innerT task.
tcs.Task.ContinueWith((lastTask) =>
{
Console.WriteLine("Continuation started");
});
// Give the user 3s to cancel the first batch of work
Console.ReadKey();
if (innerT.Status == TaskStatus.Running)
{
Console.WriteLine("Cancel requested");
cts.Cancel();
Console.ReadKey();
}
}
繰り返しますが、これは機能しますが、2 つの問題があります。
a) TaskCompletionSource の結果を決して使用せず、作業が終了したら null を設定するだけで、TaskCompletionSource を悪用しているように感じます。
b) 継続を適切に接続するために、作成されたタスクではなく、前の作業単位の一意の TaskCompletionSource のハンドルを保持する必要があります。これは技術的には可能ですが、やはりぎこちなく奇妙に感じます。
ここからどこへ行く?
繰り返しますが、私の質問は次のとおりです。これらの方法のいずれかがこの問題に取り組むための「正しい」方法ですか、それとも、長時間実行されているタスクを途中で中止してすぐに継続を開始できる、より正確でエレガントなソリューションがありますか? 私の好みは影響の少ないソリューションですが、それが正しいことであれば、大規模なリファクタリングを喜んで引き受けます。
あるいは、TPL はジョブに適したツールでさえあるのでしょうか、それともより優れたタスク キューイング メカニズムが欠けているのでしょうか。私のターゲット フレームワークは .NET 4.0 です。