この簡単なサンプルコードを考えてみましょう。
var cts = new CancellationTokenSource();
var items = Enumerable.Range(1, 20);
var results = items.AsParallel().WithCancellation(cts.Token).Select(i =>
{
double result = Math.Log10(i);
return result;
});
try
{
foreach (var result in results)
{
if (result > 1)
cts.Cancel();
Console.WriteLine($"result = {result}");
}
}
catch (OperationCanceledException e)
{
if (cts.IsCancellationRequested)
Console.WriteLine($"Canceled");
}
並列結果の結果のそれぞれについて、結果を次のように出力します。 result > 1
このコード出力は次のようなものです。
result = 0.9030899869919435
result = 0.8450980400142568
result = 0.7781512503836436
result = 0
result = 0.6020599913279624
result = 0.47712125471966244
result = 0.3010299956639812
result = 0.6989700043360189
result = 0.9542425094393249
result = 1
result = 1.0413926851582251 <-- This is normal
result = 1.2041199826559248 <-- Why it prints this value (and below)
result = 1.0791812460476249
result = 1.2304489213782739
result = 1.1139433523068367
result = 1.255272505103306
result = 1.146128035678238
result = 1.2787536009528289
result = 1.1760912590556813
result = 1.3010299956639813
Canceled
私の質問は、なぜ1を超える値を出力し続けるのですか?Cancel()
トークンがプロセスを終了することを期待していました。
アップデート1
@ mike-sの回答が提案されました:
ループ内(ループを中止する手段として)または長い操作の前にキャンセルトークンをチェックすることも役立ちます。
小切手を追加してみました
foreach (var result in results)
{
if (result > 1)
cts.Cancel();
if (!cts.IsCancellationRequested) //<----Check the cancellation token before printing
Console.WriteLine($"result = {result}");
}
それでも同じ結果の出力が得られます。
私の質問は、なぜ1を超える値を出力し続けるのですか?
あなたが100の空港から100の飛行機を飛ばすために100人のパイロットを雇ったと想像してください。それらの束が離陸し、「すべてのフライトをキャンセルします」というメッセージを送信します。さて、あなたがそのメッセージを送るとき、離陸速度で滑走路にたくさんの飛行機があります、そして、メッセージはそれらが空中にある後に到着します。これらのフライトはキャンセルされません!
マルチスレッドプログラミングについて知っておくべき最も重要なことを発見しています。起こっていることのすべての可能な順序が発生する可能性があるかのように推論する必要があります。これには、思ったよりも遅れて届くメッセージも含まれます。
特に、あなたの問題は、長い作業を並列化するように設計された並列化メカニズムの乱用の結果です。停止するメッセージを送信するよりも実行に時間がかからない一連のタスクを作成しました。その場合、一部のタスクが停止するように指示された後に完了することは驚くべきことではありません。
トークンでCancel()を呼び出すと、プロセスが終了することを期待していました。
あなたの期待は完全に、完全に間違っています。その期待は決して現実に一致しないので、それを期待するのをやめてください。キャンセルトークンは、都合のよいときに操作をキャンセルする要求です。スレッドやプロセスを終了するのではありません。
ただし、場合でもなかったのスレッドを終了し、あなたはまだ、この動作を観察します。スレッドの終了は他のイベントと同様のイベントであり、そのイベントは瞬時ではありません。実行には時間がかかり、他のスレッドはそのスレッドの終了が実行されている間も作業を続行できます。
「都合の良いときに操作をキャンセルするリクエスト」の「便利」とはどういう意味ですか?
一歩後退しましょう。
実行する作業が非常に短い場合は、それをタスクとして表す必要はありません。ただ仕事をしてください!一般に、作業にかかる時間が約30ミリ秒未満の場合は、作業を行ってください。
したがって、すべてのタスクに長い時間がかかると仮定しましょう。
では、なぜタスクに時間がかかるのでしょうか。一般的に2つの理由があります:
別のシステムがいくつかのタスクを完了するのを待っています。ネットワークパケットやディスクの読み取りなどを待っています。
大量の計算があり、CPUが飽和状態になっています。
最初の状況にあると仮定します。並列化は役に立ちますか?いいえ。郵便で荷物を待っている場合、1人、2人、10人、または100人を雇っても、荷物が早く届くわけではありません。
しかし、それは2番目のケースには役立ちます。マシンに追加のCPUがある場合、約半分の時間で問題を解決するために2つのCPUを割り当てることができます。
したがって、タスクを並列化する場合、それはCPUが多くの作業を行っているためであると推測できます。
すごい。さて、「CPUは多くの仕事をする」という性質は何ですか?ほとんどの場合、どこかにループがあります。
では、どうすればタスクをキャンセルできますか?スレッドを終了してタスクをキャンセルすることはありません。タスクにキャンセルを依頼します。適切に設計されたタスクはキャンセルトークンを受け取り、そのループで、キャンセルトークンがタスクがキャンセルされたことを示しているかどうかを確認します。キャンセルは協力的です。タスクは協力して、キャンセルされたかどうかを確認するタイミングを決定する必要があります。
キャンセルされたかどうかを確認するのは作業であり、実際のタスクから時間がかかる作業であることに注意してください。キャンセルされたかどうかを確認するために半分の時間を費やすと、タスクにかかる時間は2倍になります。そして、覚えて、タスクを並列化の点である、それは半分の長取る作るので、倍増それがタスクを実行するのにかかる時間は、非スターターです。
したがって、ほとんどのタスクは、キャンセルされたかどうかをループ全体で毎回チェックするわけではありません。適切に設計されたタスクは、数ナノ秒ごとではなく、数ミリ秒ごとにチェックします。
それが「キャンセルとは都合の良いときにやめたいというリクエスト」という意味です。タスクは、正しく記述されていれば、応答性とパフォーマンスのバランスをとるために、キャンセルをチェックするのに適した時期を知っている必要があります。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加