web-dev-qa-db-ja.com

.NETでスレッドを完全に終了することに関する質問

私はThread.Abort()がこのトピックで読んだ多数の記事から悪であると理解しているので、現在、よりクリーンな方法で置き換えるために、私のアボートのリッピングを行っています。そして、stackoverflowでここにいる人々のユーザー戦略を比較した後、を読んだ後)」 方法:スレッドを作成および終了する(C#プログラミングガイド) "MSDNからのアプローチはどちらも非常に同じです-これは_volatile bool_アプローチチェック戦略を使用することです。これはナイスですが、まだいくつか質問があります。..

すぐにここで目立っているのは、クランチコードのループを実行している単純なワーカープロセスがない場合です。たとえば、私のプロセスはバックグラウンドファイルアップローダープロセスです。実際、各ファイルをループ処理するので、これは何かです。すべてのループ反復をカバーするwhile (!_shouldStop)を上部に追加できることを確認してください。しかし、次のループイテレーションに到達する前に発生するビジネスプロセスがもっとたくさんあります。ワーカー関数全体でループを4〜5行ごとにループさせる必要があると言わないでください。

もっと良い方法があることを本当に望んでいます。実際にこれが正しいか(そして唯一の)アプローチであるか、または私が目標を達成するために過去に使用した戦略である場合、誰かが私にアドバイスしてもらえますか?.

ありがとうギャング。

さらに読む: これらすべてSO応答 ワーカースレッドがループすることを前提としています。それは私には快適ではありません。 ?

58
GONeale

残念ながら、より良い選択肢はないかもしれません。それは本当にあなたの特定のシナリオに依存します。アイデアは、安全なポイントでスレッドを適切に停止することです。それがThread.Abortが良くない理由の核心です。安全な場所で発生することが保証されていないためです。停止メカニズムをコードに振りかけることにより、安全ポイントを効果的に手動で定義できます。これは、協調キャンセルと呼ばれます。これを行うには、基本的に4つの広範なメカニズムがあります。状況に最適なものを選択できます。

停止フラグをポーリングする

この方法についてはすでに述べました。これはかなり一般的なものです。アルゴリズムの安全なポイントでフラグを定期的にチェックし、信号が送られたら救済します。標準的なアプローチは、変数volatileをマークすることです。それが不可能または不便な場合は、lockを使用できます。ローカル変数をvolatileとしてマークすることはできないので、たとえば、ラムダ式がクロージャを介してそれをキャプチャする場合、必要なメモリバリアを作成する別の方法に頼らなければならないことに注意してください。この方法について言う必要のあるものは他にたくさんありません。

TPLで新しいキャンセルメカニズムを使用する

これは、TPLの新しいキャンセルデータ構造を使用することを除いて、停止フラグのポーリングに似ています。依然として協調的なキャンセルパターンに基づいています。 CancellationTokenを取得し、定期的にIsCancellationRequestedをチェックする必要があります。キャンセルをリクエストするには、最初にトークンを提供したCancelCancellationTokenSourceを呼び出します。新しいキャンセルメカニズムでできることはたくさんあります。 here の詳細を読むことができます。

待機ハンドルを使用する

このメソッドは、ワーカースレッドが特定の間隔で待機する必要がある場合、または通常の動作中に信号を待機する必要がある場合に役立ちます。たとえば、Set a ManualResetEventを使用して、スレッドに停止する時間を知らせることができます。イベントがシグナルされたかどうかを示すWaitOneを返すbool関数を使用して、イベントをテストできます。 WaitOneは、その時間内にイベントが通知されなかった場合に呼び出しが戻るのを待つ時間を指定するパラメーターを取ります。 Thread.Sleepの代わりにこの手法を使用して、同時に停止指示を取得できます。スレッドが待機する必要がある他のWaitHandleインスタンスがある場合にも役立ちます。 WaitHandle.WaitAnyを呼び出して、すべてのイベント(停止イベントを含む)を1回の呼び出しで待機できます。プログラムのフローをより細かく制御できるため、Thread.Interruptを呼び出すよりもイベントを使用するほうがよい場合があります(Thread.Interruptは例外をスローするため、実行するにはtry-catchブロックを戦略的に配置する必要があります必要なクリーンアップ)。

特殊なシナリオ

非常に特殊化された停止メカニズムを持ついくつかの一時的なシナリオがあります。それらをすべて列挙することは、この回答の範囲外であることは間違いありません(ほぼ不可能になることはありません)。ここで私が意味することの良い例は、Socketクラスです。スレッドがSendまたはReceiveの呼び出しでブロックされている場合、Closeを呼び出すと、ブロックの呼び出しが事実上ブロック解除されたときにソケットに割り込みます。 BCLには、スレッドをブロック解除するために同様の手法を使用できる他の領域がいくつかあると確信しています。

Thread.Interruptを介してスレッドを中断します

ここでの利点は、単純であり、コードに実際に何かを振りかけることに集中する必要がないことです。欠点は、アルゴリズムのどこに安全点があるかをほとんど制御できないことです。理由は、Thread.Interruptは、BCLブロッキング呼び出しのいずれかの内部に例外を挿入することにより機能するためです。これらにはThread.SleepWaitHandle.WaitOneThread.Joinなどが含まれます。したがって、それらを配置する場所について賢明でなければなりません。ただし、ほとんどの場合、アルゴリズムはそれらがどこに行くかを指示します。特に、アルゴリズムがこれらのブロッキング呼び出しのいずれかにほとんどの時間を費やしている場合、それは通常は問題ありません。アルゴリズムがBCLのブロッキング呼び出しの1つを使用しない場合、このメソッドは機能しません。ここでの理論は、ThreadInterruptExceptionは.NET待機呼び出しからのみ生成されるため、安全な時点では可能性が高いであるということです。少なくとも、スレッドがアンマネージコードになったり、クリティカルセクションから脱出したりして、ダングリングロックを取得状態のままにすることはできません。これはThread.Abortよりも侵襲的ではありませんが、どの呼び出しがそれに応答するかが明確ではなく、多くの開発者がそのニュアンスに不慣れであるため、使用をお勧めしません。

101
Brian Gideon

残念ながら、マルチスレッドでは、クリーンさのために「スナップ」を妥協しなければならないことがよくあります... Interruptであればすぐにスレッドを終了できますが、あまりきれいではありません。いいえ、4〜5行ごとに_shouldStopチェックを振りかける必要はありませんが、スレッドに割り込みが発生した場合は、例外を処理してクリーンな方法でループを終了する必要があります。

更新

ループスレッドではない場合(つまり、おそらく長時間実行される非同期操作または入力操作用の何らかのタイプのブロックを実行するスレッド)、Interruptを実行できますが、ThreadInterruptedExceptionおよびスレッドを完全に終了します。あなたが読んでいる例は非常に適切だと思います。

更新2.0

はい、例があります...参照したリンクに基づいた例を示します。

public class InterruptExample
{
    private Thread t;
    private volatile boolean alive;

    public InterruptExample()
    {
        alive = false;

        t = new Thread(()=>
        {
            try
            {
                while (alive)
                {
                    /* Do work. */
                }
            }
            catch (ThreadInterruptedException exception)
            {
                /* Clean up. */
            }
        });
        t.IsBackground = true;
    }

    public void Start()
    {
        alive = true;
        t.Start();
    }


    public void Kill(int timeout = 0)
    {
        // somebody tells you to stop the thread
        t.Interrupt();

        // Optionally you can block the caller
        // by making them wait until the thread exits.
        // If they leave the default timeout, 
        // then they will not wait at all
        t.Join(timeout);
    }
}
12
Kiril

キャンセルがあなたが構築しているものの要件である場合、それはあなたのコードの残りと同じくらい尊重して扱われるべきです-それはあなたが設計しなければならないものかもしれません。

スレッドが常に2つのことのいずれかを実行していると仮定しましょう。

  1. CPUバウンドの何か
  2. カーネルを待っています

問題のスレッドにCPUがバインドされている場合、おそらく救済チェックを挿入するのに適した場所があります。他の人のコードを呼び出して長時間実行されるCPUにバインドされたタスクを実行する場合、外部コードを修正し、プロセスの外に移動する必要があります(スレッドの中止は悪ですが、プロセスの中止は明確で安全です) )など.

カーネルを待っている場合、おそらく待機に関係するハンドル(またはfd、またはmachポートなど)があります。通常、関連するハンドルを破棄すると、カーネルはすぐに何らかのエラーコードを返します。 .net/Java/etcにいる場合。例外が発生する可能性があります。 Cでは、システムコールの障害を処理するためにすでに用意されているコードはすべて、アプリの重要な部分までエラーを伝播します。どちらにしても、新しいコードをあちこちに散らさずに、低レベルの場所からかなりきれいに、非常にタイムリーに抜け出します。

この種のコードでよく使用する戦術は、閉じる必要があるハンドルのリストを追跡し、その後、中止関数に「キャンセル」フラグを設定してから閉じることです。関数が失敗すると、フラグをチェックして、特定の例外/ errnoが原因ではなく、キャンセルが原因で失敗を報告できます。

キャンセルの許容可能な粒度がサービスコールのレベルであることを暗示しているようです。これはおそらく良い考えではありません。バックグラウンド作業を同期的にキャンセルし、古いバックグラウンドスレッドをフォアグラウンドスレッドから結合する方がはるかに優れています。それはとてもきれいだからです:

  1. 古いbgworkスレッドが予期せぬ遅延の後に元に戻る場合の競合状態のクラスを回避します。

  2. バックグラウンドスレッドのハングの影響を非表示にすることにより、バックグラウンドプロセスのハングによって引き起こされる潜在的な隠れたスレッド/メモリリークを回避します。

このアプローチが怖い理由は2つあります。

  1. 自分のコードをタイムリーに中止できるとは思わないでしょう。キャンセルがアプリの要件である場合、実際に行う必要がある決定は、リソース/ビジネスの決定です:ハックを行うか、問題をきれいに修正します。

  2. 制御できないため、呼び出しているコードを信頼しません。本当に信頼できない場合は、プロセス外に移動することを検討してください。これを含め、多くの種類のリスクからの分離がはるかに良くなります。

9
blucz

おそらく、問題の一部は、そのような長いメソッド/ whileループがあることです。スレッドの問題があるかどうかに関係なく、それをより小さな処理ステップに分解する必要があります。これらのステップがAlpha()、Bravo()、Charlie()、Delta()であるとします。

次のようなことができます:

    public void MyBigBackgroundTask()
    {
        Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta };
        int workStepSize = 0;
        while (!_shouldStop)
        {
            tasks[workStepSize++]();
            workStepSize %= tasks.Length;
        };
    }

そう、それは無限にループしますが、各ビジネスステップ間で停止する時間であるかどうかをチェックします。

2
Brent Arias

ループが至る所に散らばる必要はありません。外側のwhileループは、停止するように指示されているかどうかを確認し、もしそうであれば、別の反復を行いません...

まっすぐな「何かを実行して閉じる」スレッド(ループを含まない)がある場合は、スレッド内の各主要スポットの前または後に_shouldStopブール値をチェックするだけです。そうすれば、それを継続するか、または救済する必要があるかがわかります。

例えば:

public void DoWork() {

  RunSomeBigMethod();
  if (_shouldStop){ return; }
  RunSomeOtherBigMethod();
  if (_shouldStop){ return; }
  //....
}

2
NotMe

最良の答えは、スレッドで何をしているかに大きく依存します。

  • あなたが言ったように、ほとんどの答えは共有されたブール値を数行ごとにポーリングすることを中心に展開します。気に入らないかもしれませんが、これは多くの場合最も単純なスキームです。生活を楽にしたい場合は、ThrowIfCancelled()のようなメソッドを書くことができます。このメソッドは、終了すると何らかの例外をスローします。純粋主義者は、これは制御フローの例外を使用する(息をのむ)と言いますが、それでもキャセルは例外的なものです。

  • IO操作(ネットワーク関連など)を実行している場合、非同期操作を使用してすべてを実行することを検討できます。

  • 一連の手順を実行している場合、IEnumerableトリックを使用してステートマシンを作成できます。例:

<

abstract class StateMachine : IDisposable
{
    public abstract IEnumerable<object> Main();

    public virtual void Dispose()
    {
        /// ... override with free-ing code ...
   }

   bool wasCancelled;

   public bool Cancel()
   {
     // ... set wasCancelled using locking scheme of choice ...
    }

   public Thread Run()
   {
       var thread = new Thread(() =>
           {
              try
              {
                if(wasCancelled) return;
                foreach(var x in Main())
                {
                    if(wasCancelled) return;
                }
              }
              finally { Dispose(); }
           });
       thread.Start()
   }
}

class MyStateMachine : StateMachine
{
   public override IEnumerabl<object> Main()
   {
       DoSomething();
       yield return null;
       DoSomethingElse();
       yield return null;
   }
}

// then call new MyStateMachine().Run() to run.

>

オーバーエンジニアリング?使用するステートマシンの数によって異なります。 1つだけの場合は、はい。あなたが100を持っているなら、多分そうではありません。トリッキーすぎる?まあ、それは依存します。このアプローチのもう1つのボーナスは、(わずかな変更を加えて)操作をTimer.tickコールバックに移動し、理にかなっている場合はスレッド化を完全に無効にすることです。

そして、bluczが言うこともすべてやります。

2
Glenn

それ以外の場合はループが属さないwhileループを追加する代わりに、if (_shouldStop) CleanupAndExit();のようなものを追加するのが理にかなっているところで追加します。毎回の操作の後に確認したり、コードをあちこちに散らしたりする必要はありません。代わりに、各チェックをその時点でスレッドを終了するチャンスと考え、これを念頭に置いて戦略的に追加してください。

2
Adrian Lopez

これらすべてのSOの応答は、ワーカースレッドがループすることを前提としています。

コードに長い時間をかける方法はあまりありません。ループは非常に重要なプログラミング構造です。ループせずにコードを作成するのに時間がかかるには、巨大の量のステートメントが必要です。数十万人。

または、ループを実行している他のコードを呼び出します。はい、そのコードをオンデマンドで停止させるのは難しいです。それはうまくいきません。

1
Hans Passant