web-dev-qa-db-ja.com

スレッドの中止のようなタスクを中止することは可能ですか(Thread.Abortメソッド)?

次のようなスレッドを中止できます。

Thread thread = new Thread(SomeMethod);
.
.
.
thread.Abort();

ただし、キャンセルメカニズムではなく、同じ方法で(.Net 4.0で)タスクを中止できますか。 タスクをすぐに強制終了したい

56
Amir Karimi
  1. Thread.Abort()を使用しないでください
  2. タスクはキャンセルできますが、中止できません。

Thread.Abort() メソッドは(厳しく)非推奨です。

停止すると、スレッドとタスクの両方が連携する必要があります。そうしないと、システムが不安定/未定義状態のままになるリスクがあります。

プロセスを実行して外部から強制終了する必要がある場合、唯一の安全なオプションは、別のAppDomainで実行することです。

29
Henk Holterman

スレッドアボートを使用しないというガイダンスは議論の余地があります。私はまだそれのための場所があると思うが、例外的な状況にある。ただし、常にそれを中心に設計し、最後の手段として考えてください。

例;

ブロッキング同期Webサービスに接続する単純なWindowsフォームアプリケーションがあります。その中で、並列ループ内でWebサービスの関数を実行します。

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    Thread.Sleep(120000); // pretend web service call

});

この例では、ブロッキングコールが完了するまでに2分かかります。ここで、MaxDegreeOfParallelismをProcessorCountに設定します。 iListOfItemsには、処理するアイテムが1000個あります。

ユーザーがプロセスボタンをクリックすると、ループが開始されます。iListOfItemsコレクションの1000個のアイテムに対して「最大」20個のスレッドが実行されます。各反復は、独自のスレッドで実行されます。 Parallel.ForEachによって作成される場合、各スレッドはフォアグラウンドスレッドを利用します。これは、メインアプリケーションのシャットダウンに関係なく、すべてのスレッドが終了するまでアプリドメインが存続することを意味します。

ただし、ユーザーはフォームを閉じるなど、何らかの理由でアプリケーションを閉じる必要があります。これらの20個のスレッドは、1000個すべてのアイテムが処理されるまで実行を続けます。タスクマネージャーで確認するとわかるように、これはこのシナリオでは理想的ではありません。ユーザーが期待するようにアプリケーションが終了せず、舞台裏で実行され続けるためです。

ユーザーがアプリを再構築しようとすると(VS 2010)、exeがロックされていると報告された後、タスクマネージャーに移動してそれを強制終了するか、1000個すべてのアイテムが処理されるまで待つ必要があります。

言ったことであなたを責めることはないでしょうが、もちろんです! CancellationTokenSource オブジェクトを使用してCancelを呼び出してこれらのスレッドをキャンセルする必要がありますが、.net 4.0の時点でこれにはいくつかの問題があります。第一に、これはまだスレッド例外を引き起こし、スレッド例外が続くスレッド例外を引き起こさないため、アプリドメインは代わりにスレッドが正常に終了するのを待つ必要があり、これは最後のブロッキング呼び出しを待つことを意味します、最終的にpo.CancellationToken.ThrowIfCancellationRequestedを呼び出す最後の実行反復(スレッド)になります。この例では、フォームが閉じられ、キャンセルが呼び出された場合でも、アプリドメインが最大2分間存続できることを意味します。

CancellationTokenSourceでCancelを呼び出しても、処理スレッドで例外がスローされないことに注意してください。例外は、スレッドの中止と同様にブロッキング呼び出しを中断し、実行を停止します。例外は、他のすべてのスレッド(同時反復)が最終的に完了して戻るときに準備ができてキャッシュされ、例外は開始スレッド(ループが宣言されている)でスローされます。

CancellationTokenSourceオブジェクトで Cancel オプションを使用するために、notを選択しました。これは無駄であり、間違いなく、例外によってコードのフローを制御するよく知られているアンチパターンに違反します。

代わりに、単純なスレッドセーフプロパティ、つまりBool stopExecutingを実装することはほぼ間違いなく「より適切」です。次に、ループ内で、stopExecutingの値を確認します。値が外部の影響によってtrueに設定されている場合は、代替パスを使用して正常に閉じることができます。 cancelを呼び出すべきではないため、これは CancellationTokenSource.IsCancellationRequested のチェックを除外します。

ループ内で条件が適切な場合、次のようなもの。

if(loopState.ShouldExitCurrentIteration || loopState.IsExceptional || stopExecuting){loopState.Stop(); return;}

これで、反復は「制御された」方法で終了し、さらに反復を終了しますが、先ほど述べたように、これは各反復内で行われる長時間の呼び出しとブロック呼び出しを待つ必要があるという問題にはほとんど役立ちません(これは、各スレッドが停止する必要があるかどうかをチェックするオプションに到達する前に完了する必要があるためです。

要約すると、ユーザーがフォームを閉じると、20個のスレッドはstopExecutingを介して停止するように通知されますが、長時間実行される関数呼び出しの実行が終了したときにのみ停止します。

アプリケーションドメインが常にアクティブであり、すべてのフォアグラウンドスレッドが完了したときにのみ解放されるという事実については何もできません。そして、これは、ループ内で行われたブロッキング呼び出しの完了を待機することに関連する遅延があることを意味します。

ブロッキングコールを中断できるのは真のスレッドアボートのみであり、システムを不安定/未定義状態のままにしておくのは、アボートスレッドの例外ハンドラーで可能な限り最善の方法で軽減する必要があります。それが適切かどうかは、プログラマが維持することを選択したリソースハンドルと、スレッドの最終ブロックでそれらを閉じるのがどれだけ簡単かによって、プログラマが決定する問題です。半回避策としてキャンセル時に終了するトークンで登録することができます.

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    using (cts.Token.Register(Thread.CurrentThread.Abort))
    {
        Try
        {
           Thread.Sleep(120000); // pretend web service call          
        }
        Catch(ThreadAbortException ex)
        {
           // log etc.
        }
        Finally
        {
          // clean up here
        }
    }

});

ただし、これにより、宣言スレッドで例外が発生します。

考慮すべきことはすべて、parallel.loopコンストラクトを使用した割り込みブロッキング呼び出しは、オプションのメソッドであり、ライブラリのより不明瞭な部分の使用を回避できた可能性があります。しかし、宣言メソッドでキャンセルして例外をスローすることを避けるオプションがない理由は、私が見落としている可能性があると思います。

41

ただし、キャンセルメカニズムではなく、同じ方法で(.Net 4.0で)タスクを中止できますか。 タスクをすぐに強制終了したい

他の回答者は、それをしないように言っています。しかし、はい、あなたはcanできます。タスクのキャンセルメカニズムによって呼び出されるデリゲートとしてThread.Abort()を指定できます。これを設定する方法は次のとおりです。

class HardAborter
{
  public bool WasAborted { get; private set; }
  private CancellationTokenSource Canceller { get; set; }
  private Task<object> Worker { get; set; }

  public void Start(Func<object> DoFunc)
  {
    WasAborted = false;

    // start a task with a means to do a hard abort (unsafe!)
    Canceller = new CancellationTokenSource();

    Worker = Task.Factory.StartNew(() => 
      {
        try
        {
          // specify this thread's Abort() as the cancel delegate
          using (Canceller.Token.Register(Thread.CurrentThread.Abort))
          {
            return DoFunc();
          }
        }
        catch (ThreadAbortException)
        {
          WasAborted = true;
          return false;
        }
      }, Canceller.Token);
  }

  public void Abort()
  {
    Canceller.Cancel();
  }

}

免責事項:これをしないでください。

禁止事項の例を次に示します。

 var doNotDoThis = new HardAborter();

 // start a thread writing to the console
 doNotDoThis.Start(() =>
    {
       while (true)
       {
          Thread.Sleep(100);
          Console.Write(".");
       }
       return null;
    });


 // wait a second to see some output and show the WasAborted value as false
 Thread.Sleep(1000);
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted);

 // wait another second, abort, and print the time
 Thread.Sleep(1000);
 doNotDoThis.Abort();
 Console.WriteLine("Abort triggered at " + DateTime.Now);

 // wait until the abort finishes and print the time
 while (!doNotDoThis.WasAborted) { Thread.CurrentThread.Join(0); }
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted + " at " + DateTime.Now);

 Console.ReadKey();

output from sample code

38
jltrem

スレッドを終了するのが悪いことは誰もが(できれば)知っています。問題は、呼び出しているコードを所有していない場合です。このコードがdo/while無限ループで実行されている場合、それ自体がいくつかのネイティブ関数を呼び出すなど、基本的にスタックしています。これがあなた自身のコードの終了、停止、または破棄の呼び出しで発生した場合、悪者を撃ち始めても大丈夫です(あなた自身が悪者にならないように)。

そのため、プールからのスレッドやCLRによって作成されたスレッドではなく、独自のネイティブスレッドを使用する2つのブロッキング関数を作成しました。タイムアウトが発生すると、スレッドを停止します。

// returns true if the call went to completion successfully, false otherwise
public static bool RunWithAbort(this Action action, int milliseconds) => RunWithAbort(action, new TimeSpan(0, 0, 0, 0, milliseconds));
public static bool RunWithAbort(this Action action, TimeSpan delay)
{
    if (action == null)
        throw new ArgumentNullException(nameof(action));

    var source = new CancellationTokenSource(delay);
    var success = false;
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            action();
            success = true;
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return success;
}

// returns what's the function should return if the call went to completion successfully, default(T) otherwise
public static T RunWithAbort<T>(this Func<T> func, int milliseconds) => RunWithAbort(func, new TimeSpan(0, 0, 0, 0, milliseconds));
public static T RunWithAbort<T>(this Func<T> func, TimeSpan delay)
{
    if (func == null)
        throw new ArgumentNullException(nameof(func));

    var source = new CancellationTokenSource(delay);
    var item = default(T);
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            item = func();
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return item;
}

[DllImport("kernel32")]
private static extern bool TerminateThread(IntPtr hThread, int dwExitCode);

[DllImport("kernel32")]
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes, IntPtr dwStackSize, Delegate lpStartAddress, IntPtr lpParameter, int dwCreationFlags, out int lpThreadId);

[DllImport("kernel32")]
private static extern bool CloseHandle(IntPtr hObject);

[DllImport("kernel32")]
private static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);
3
Simon Mourier

スレッドを中止することは可能ですが、実際には、そうすることはほとんど常に非常に悪い考えです。スレッドを中断すると、スレッドはそれ自体をクリーンアップする機会を与えられず、リソースは削除されず、状態は不明になります。

実際には、スレッドを中止する場合は、プロセスを強制終了するときにのみ行う必要があります。悲しいことに、あまりにも多くの人々が、ThreadAbortは何かを止めて継続するための実行可能な方法だと考えていますが、そうではありません。

タスクはスレッドとして実行されるため、タスクに対してThreadAbortを呼び出すことができますが、一般的なスレッドと同様に、最後の手段を除いてこれを行うことはほとんどありません。

2