18

私たちのアプリケーションは、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 です。

4

1 に答える 1

9

ここでの本当の問題は、DoWork で実行時間の長い呼び出しがキャンセルに対応していないことです。私の理解が正しければ、あなたがここで行っていることは、長時間実行されている作業を実際にキャンセルすることではなく、単に継続の実行を許可し、キャンセルされたタスクで作業が完了すると、結果を無視することです。たとえば、内側のタスク パターンを使用して CrunchNumbers() を呼び出した場合、これには数分かかります。外側のタスクをキャンセルすると続行できますが、CrunchNumbers() は完了するまでバックグラウンドで実行され続けます。

長時間の通話でキャンセルをサポートする以外に、これを回避する実際の方法はないと思います。多くの場合、これは不可能です (API 呼び出しをブロックし、API のキャンセルをサポートしていない可能性があります)。この場合、実際には API の欠陥です。キャンセル可能な方法で操作を実行するために使用できる代替 API 呼び出しがあるかどうかを確認できます。これに対するハック アプローチの 1 つは、Task の開始時に Task によって使用されている基になる Thread への参照を取得し、Thread.Interrupt を呼び出すことです。これにより、スレッドがさまざまなスリープ状態から復帰し、終了できるようになりますが、厄介な方法になる可能性があります。最悪の場合、Thread.Abort を呼び出すこともできますが、これはさらに問題があり、お勧めできません。


これはデリゲートベースのラッパーへの刺し傷です。テストされていませんが、うまくいくと思います。回答が機能し、修正/改善がある場合は、回答を自由に編集してください。

public sealed class AbandonableTask
{
    private readonly CancellationToken _token;
    private readonly Action _beginWork;
    private readonly Action _blockingWork;
    private readonly Action<Task> _afterComplete;

    private AbandonableTask(CancellationToken token, 
                            Action beginWork, 
                            Action blockingWork, 
                            Action<Task> afterComplete)
    {
        if (blockingWork == null) throw new ArgumentNullException("blockingWork");

        _token = token;
        _beginWork = beginWork;
        _blockingWork = blockingWork;
        _afterComplete = afterComplete;
    }

    private void RunTask()
    {
        if (_beginWork != null)
            _beginWork();

        var innerTask = new Task(_blockingWork, 
                                 _token, 
                                 TaskCreationOptions.LongRunning);
        innerTask.Start();

        innerTask.Wait(_token);
        if (innerTask.IsCompleted && _afterComplete != null)
        {
            _afterComplete(innerTask);
        }
    }

    public static Task Start(CancellationToken token, 
                             Action blockingWork, 
                             Action beginWork = null, 
                             Action<Task> afterComplete = null)
    {
        if (blockingWork == null) throw new ArgumentNullException("blockingWork");

        var worker = new AbandonableTask(token, beginWork, blockingWork, afterComplete);
        var outerTask = new Task(worker.RunTask, token);
        outerTask.Start();
        return outerTask;
    }
}
于 2011-01-20T16:00:46.897 に答える