web-dev-qa-db-ja.com

タスクをキャンセルすると例外がスローされます

私がタスクについて読んだことから、次のコードは例外をスローすることなく現在実行中のタスクをキャンセルするはずです。私は、タスクの取り消しの全体のポイントは、スレッドを中断せずに停止するようにタスクを丁寧に「求める」ことであるという印象を受けていました。

次のプログラムからの出力は次のとおりです。

ダンプ例外

[OperationCanceledException]

最後に計算された素数をキャンセルして返します。

キャンセルする際に例外を回避しようとしています。どうすればこれを達成できますか?

void Main()
{
    var cancellationToken = new CancellationTokenSource();

    var task = new Task<int>(() => {
        return CalculatePrime(cancellationToken.Token, 10000);
    }, cancellationToken.Token);

    try
    {
        task.Start();
        Thread.Sleep(100);
        cancellationToken.Cancel();
        task.Wait(cancellationToken.Token);         
    }
    catch (Exception e)
    {
        Console.WriteLine("Dumping exception");
        e.Dump();
    }
}

int CalculatePrime(CancellationToken cancelToken, object digits)
{  
    int factor; 
    int lastPrime = 0;

    int c = (int)digits;

    for (int num = 2; num < c; num++)
    { 
        bool isprime = true;
        factor = 0; 

        if (cancelToken.IsCancellationRequested)
        {
            Console.WriteLine ("Cancelling and returning last calculated prime.");
            //cancelToken.ThrowIfCancellationRequested();
            return lastPrime;
        }

        // see if num is evenly divisible 
        for (int i = 2; i <= num/2; i++)
        { 
            if ((num % i) == 0)
            {             
                // num is evenly divisible -- not prime 
                isprime = false; 
                factor = i; 
            }
        } 

        if (isprime)
        {
            lastPrime = num;
        }
    }

    return lastPrime;
}
52
Vince Panuccio

この行で明示的に例外をスローしています:

_cancelToken.ThrowIfCancellationRequested();
_

タスクを正常に終了する場合は、その行を削除するだけです。

通常、人々はこれを制御メカニズムとして使用して、余分なコードを実行することなく現在の処理を確実に中止します。また、ThrowIfCancellationRequested()を呼び出すときにキャンセルをチェックする必要はありません。機能的には次と同等です。

_if (token.IsCancellationRequested) 
    throw new OperationCanceledException(token);
_

ThrowIfCancellationRequested()を使用すると、タスクは次のようになります。

_int CalculatePrime(CancellationToken cancelToken, object digits) {
    try{
        while(true){
            cancelToken.ThrowIfCancellationRequested();

            //Long operation here...
        }
    }
    finally{
        //Do some cleanup
    }
}
_

また、トークンがキャンセルされた場合、Task.Wait(CancellationToken)は例外をスローします。このメソッドを使用するには、_Try...Catch_ブロックでWait呼び出しをラップする必要があります。

MSDN:タスクをキャンセルする方法

67
Josh

キャンセルする際に例外を回避しようとしています。

あなたはそれをするべきではありません。

OperationCanceledExceptionを投げることは、「呼び出したメソッドが取り消された」という慣用的な方法であり、TPLで表現されます。それと戦わないでください-ただ期待してください。

これはgoodのことです。これは、同じキャンセルトークンを使用して複数の操作を行ったときに、コードをペッパー化する必要がないことを意味するためです。呼び出したばかりのメソッドが実際に正常に完了したかどうか、またはキャンセルのために返されたかどうかを確認するためのすべてのレベル。あなたはcouldCancellationToken.IsCancellationRequested を使用しますが、長い目で見ればコードのエレガントさは大幅に低下します。

あなたの例には2つのコードがあり、例外をスローしていることに注意してください。

cancelToken.ThrowIfCancellationRequested()

そして、タスクが完了するまで待つ場所:

task.Wait(cancellationToken.Token);

キャンセルトークンをtask.Wait呼び出しに渡したいとは思わない、正直に言うと...otherキャンセルするコード待機中あなたがそのトークンをキャンセルしたことがわかっているので、それは無意味です-タスクが実際にキャンセルに気づいたかどうかにかかわらず、例外をスローするのはboundですありません。オプション:

  • differentキャンセルトークンを使用します(他のコードが独立して待機をキャンセルできるように)
  • タイムアウトを使用する
  • それがかかる限り待ってください
87
Jon Skeet

上記の回答のいくつかは、ThrowIfCancellationRequested()がオプションであるかのように読みます。この場合、notになります。これは、結果の最終素数を取得できないためです。 idiomatic way that "the method you called was cancelled"は、キャンセルとは(中間の)結果を破棄することを意味する場合に定義されます。キャンセルの定義が「計算を停止して最後の中​​間結果を返す」である場合は、すでにその方法を終了しています。

特にランタイムの面で利点を議論することも非常に誤解を招く可能性があります:実装されたアルゴリズムは実行時に下手です。高度に最適化されたキャンセルでも効果はありません。

最も簡単な最適化は、このループを展開し、不要なサイクルをスキップすることです。

for(i=2; i <= num/2; i++) { 
  if((num % i) == 0) { 
    // num is evenly divisible -- not prime 
    isprime = false; 
    factor = i; 
  }
} 

あなたはできる

  • 偶数ごとに(num/2)-1サイクル保存します。これは全体で50%未満(展開)です。
  • すべての素数の(num/2)-square_root_of(num)サイクルを保存します(最小の素因数の数学に従って境界を選択します)。
  • すべての非プライムに対して少なくともそれだけ節約し、さらに多くの節約を期待します。 num = 999は499ではなく1サイクルで終了します(回答が見つかった場合は中断します)。
  • もちろん全体の25%であるサイクルをさらに50%節約します(素数の計算に従ってステップを選択し、展開すると特殊なケース2が処理されます)。

これは、次のように置き換えるだけで、内側ループのサイクルの保証最小値75%(概算:90%)を節約することになります

if ((num % 2) == 0) {
  isprime = false; 
  factor = 2;
} else {
  for(i=3; i <= (int)Math.sqrt(num); i+=2) { 
    if((num % i) == 0) { 
      // num is evenly divisible -- not prime 
      isprime = false; 
      factor = i;
      break;
    }
  }
} 

はるかに高速なアルゴリズムがあります(トピックから十分離れているので説明しません)が、この最適化は非常に簡単であり、それでも私のポイントを証明しています:アルゴリズムがthis最適にはほど遠い。

8
No answer

ThrowIfCancellationRequestedではなくIsCancellationRequestedを使用する利点に関する別の注意:ContinueWithを継続オプションTaskContinuationOptions.OnlyOnCanceledとともに使用する必要がある場合、IsCancellationRequestedは条件付きContinueWith発砲。 ThrowIfCancellationRequested、ただし、willタスクのキャンセル条件を設定し、ContinueWithを起動します。

注:これは、タスクが既に実行されている場合にのみ当てはまり、タスクの開始時には当てはまりません。これが、開始とキャンセルの間にThread.Sleep()を追加した理由です。

CancellationTokenSource cts = new CancellationTokenSource();

Task task1 = new Task(() => {
    while(true){
        if(cts.Token.IsCancellationRequested)
            break;
    }
}, cts.Token);
task1.ContinueWith((ant) => {
    // Perform task1 post-cancellation logic.
    // This will NOT fire when calling cst.Cancel().
}

Task task2 = new Task(() => {
    while(true){
        cts.Token.ThrowIfCancellationRequested();
    }
}, cts.Token);
task2.ContinueWith((ant) => {
    // Perform task2 post-cancellation logic.
    // This will fire when calling cst.Cancel().
}

task1.Start();
task2.Start();
Thread.Sleep(3000);
cts.Cancel();
7
Gerard Torres