はTPL

2011-01-19 9 views
17

に長い実行中のタスクの中止当社のアプリケーションは、(潜在的に)長い作業単位を実行しているシリアル化するためにTPLを使用しています。作業(タスク)の作成はユーザ主導であり、いつでもキャンセルすることができます。応答性の高いユーザーインターフェースを得るために、現在の作業がもはや必要でない場合、私たちが行っていたことを断念し、直ちに別の作業を開始したいと考えています。それは常にtoken.IsCancellationRequestedの状態をチェックし、/場合は、キャンセルが検出された場合に救済するように単純ではありませんので、DoWork方法は、長時間実行されているコールが含まれていは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); 
    } 
} 

タスクは、このような何かをキューに登録されています。長時間実行されている作業は、タスクがキャンセルされてもタスクが継続するまで続行をブロックします。

私はこの問題を回避するために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" タスクは、私にとって非常にkludgey感じています。また、新しいタスク内のすべての実行時間の長い呼び出しの最大ラッピングを必要とすることによって、このように作業をキューに私のコードのすべての部分をリファクタリングするために私を強制するという欠点を持っています。

試み#2:、私はそれは結果だ使ったことがないことでTaskCompletionSourceを乱用してるよう

a)のそれは感じている: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つの問題を抱えています私が仕事を終えたときにnullを設定するだけです。適切に私は前作のユニークなTaskCompletionSourceの単位ではなく、そのために作成されたタスクのハンドルを維持するために必要な継続を配線するために

B)。これは技術的に可能ですが、やはり厄介で奇妙な感じです。

ここからどこへ行く ?

私の質問は、これらの方法のどちらかがこの問題に取り組むための「正しい」方法であるか、より長い間実行されているタスクを早めに中止して直ちに開始するより正確で洗練されたソリューションです継続?私の好みは、インパクトの低いソリューションですが、それが正しいことであれば、巨大なリファクタリングを引き受けることができます。

代わりには、TPLは仕事のためにも、正しい道具である、または私はより良い、タスクキューイングメカニズムをしないのです。私のターゲットフレームワークは.NET 4.0です。

+0

私もこちらに質問を:http://social.msdn.microsoft.com/Forums/en/parallelextensions/thread/d0bcb415-fb1e-42e4-90f8-c43a088537fb –

答えて

8

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

長時間の通話でキャンセルをサポートする以外に、実際の方法はありません。多くの場合、これは不可能です(API呼び出しをブロックしている可能性があり、APIのキャンセルをサポートしていません)。この場合、実際にはAPIの欠陥です。キャンセル可能な方法で操作を実行するために使用できる代替API呼び出しがあるかどうかを確認することができます。これに対する1つのハックのアプローチは、タスクが開始されたときにタスクによって使用される基底のスレッドへの参照を取得し、次に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; 
    } 
} 
+0

あなたの理解は正しいです。私たちは "CrunchNumbers"を完走させても問題ありませんが、その結果はいつでも無視されます。 –

+0

そうであれば、内部のタスクパターンが最もクリーンなアプローチだと思います。それは私の心の中で、あなたが本当にやっていることに最も論理的に結びついています。操作の一環として長時間実行されるタスクを開始し、取り消された場合、基本的にその内部タスクを「放棄」して続行します。パターンのぎこちなさを、ジェネリックヘルパークラスにカプセル化することで改善できると思います。いくつかのデザインが考えられますが、変更する必要のある実際のコードにうまく対応できるものを見つけることができます。 –

+0

ラッパーコードをありがとう - 昨日試してみるのを邪魔してしまいました。うまくいけば今日はサワーです。私は現在、試行#2のラップアップを試みています(あなたの試行は試行#1のまとめです)。いったん完了すると、あなたのコードも試してみます。 また、私はここでStephen Toubからの回答を得ました:http://social.msdn.microsoft.com/Forums/en/parallelextensions/thread/d0bcb415-fb1e-42e4-90f8-c43a088537fb –