10

次のプログラムを検討してください。

program TThreadBug;
{$APPTYPE CONSOLE}

uses
  SysUtils, Classes, Windows;

type
  TMyThread = class(TThread)
  protected
    procedure Execute; override;
  end;

procedure TMyThread.Execute;
var
  i: Integer;
begin
  for i := 1 to 5 do begin
    Writeln(i);
    Sleep(100);
  end;
end;

procedure UseTThread;
var
  Thread: TMyThread;
begin
  Writeln('TThread');
  Thread := TMyThread.Create;
  Thread.Terminate;
  Thread.WaitFor;
  Thread.Free;
  Writeln('Finished');
  Writeln;
end;

procedure UseTThreadWithSleep;
var
  Thread: TMyThread;
begin
  Writeln('TThreadWithSleep');
  Thread := TMyThread.Create;
  Sleep(100);
  Thread.Terminate;
  Thread.WaitFor;
  Thread.Free;
  Writeln('Finished');
  Writeln;
end;

begin
  UseTThread;
  UseTThreadWithSleep;
  Readln;
end.

出力は次のとおりです。

Tスレッド
終了した

TThreadWithSleep
1
2
3
4
5
終了した

そのため、何らかの理由で、メイン スレッドはワーカー スレッドを終了して待機する前に、任意の時間待機する必要があるようです。これは のバグだと思いTThreadますか? これを回避する方法はありますか? スレッドが開始したことを (イベントを使用して) 通知するようにすれば、問題は回避されると思います。しかし、それは私を汚く感じさせます。

4

4 に答える 4

15

それはバグTThreadとも設計上の欠陥とも言え、この問題は何度も議論されました。たとえば、http://sergworks.wordpress.com/2011/06/25/sleep-sort-and-tthread-corner-case/を参照してください。

問題は、TThread.Terminatedフラグの設定が早すぎるTThread.Executeとメソッドが呼び出されないことです。したがって、あなたの場合、TThread.Terminatebeforeを呼び出さないでくださいTThread.WaitFor

于 2013-02-23T15:22:27.967 に答える
5

これが発生する理由は、Sergの回答で十分に答えられていると思いますが、とにかく通常はThread.Terminateを呼び出すべきではないと思います。スレッドを終了させたい場合、たとえばアプリケーションが閉じているときに、それを呼び出す唯一の理由。終了するまで待ちたい場合は、WaitFor(またはWaitForSingleObject)を呼び出すことができます。これが可能なのは、スレッドのハンドルがコンストラクターですでに作成されているため、すぐに呼び出すことができるためです。

また、これらのスレッドでFreeOnTerminateをtrueに設定しました。ただ彼らを走らせて彼ら自身を解放させなさい。それらの通知を実行したい場合は、WaitForまたはOnTerminateイベントのいずれかを使用できます。

これは、ブロックする方法でキューを空にする一連のワーカースレッドの例です。

デビッド、これは必要ないと思いますが、他の誰かが例に満足しているかもしれません。一方で、TThreadの不十分な実装について怒鳴る変更を加えるためだけに、この質問をしたのではないでしょうか。;-)

まず、Queueクラス。それは実際には伝統的なキューではないと思います。実際のマルチスレッドキューでは、処理がアクティブな場合でも、いつでもキューに追加できるはずです。このキューでは、アイテムを事前に埋めてから、-blocking-runメソッドを呼び出す必要があります。また、処理されたアイテムはキューに戻されます。

type
  TQueue = class
  strict private
    FNextItem: Integer;
    FRunningThreads: Integer;
    FLock: TCriticalSection;
    FItems: TStrings; // Property...
  private

    // Signal from the thread that it is started or stopped.
    // Used just for indication, no real functionality depends on this.
    procedure ThreadStarted;
    procedure ThreadEnded;

    // Pull the next item from the queue.
    function Pull(out Item: Integer; out Value: string): Boolean;

    // Save the modified value back in the queue.
    procedure Save(Item: Integer; Value: string);

  public
    property Items: TStrings read FItems;
    constructor Create;
    destructor Destroy; override;

    // Process the queue. Blocking: Doesn't return until every item in the
    // queue is processed.
    procedure Run(ThreadCount: Integer);

    // Statistics for polling.
    property Item: Integer read FNextItem;
    property RunningThreads: Integer read FRunningThreads;
  end;

次に、コンシューマースレッド。それはわかりやすくて簡単です。キューへの参照と、キューが空になるまで実行されるexecuteメソッドがあります。

  TConsumer = class(TThread)
  strict private
    FQueue: TQueue;
  protected
    procedure Execute; override;
  public
    constructor Create(AQueue: TQueue);
  end;

ここに、このあいまいな「キュー」の実装が表示されます。主な方法はプルと保存です。これらは、コンシューマーが次のアイテムをプルし、処理された値を保存するために使用します。

もう1つの重要な方法は、実行です。これは、指定された数のワーカースレッドを開始し、すべてが終了するまで待機します。したがって、これは実際にはブロッキングメソッドであり、キューが空になった後にのみ戻ります。ここではWaitForMultipleObjectsを使用しています。これにより、トリックを追加する必要がある前に、最大64スレッドまで待機できます。これは、質問のコードでWaitForSingleObjectを使用するのと同じです。

Thread.Terminateが呼び出されない方法をご覧ください。

{ TQueue }

constructor TQueue.Create;
// Context: Main thread
begin
  FItems := TStringList.Create;
  FLock := TCriticalSection.Create;
end;

destructor TQueue.Destroy;
// Context: Main thread
begin
  FLock.Free;
  FItems.Free;
  inherited;
end;

function TQueue.Pull(out Item: Integer; out Value: string): Boolean;
// Context: Consumer thread
begin
  FLock.Acquire;
  try
    Result := FNextItem < FItems.Count;
    if Result then
    begin
      Item := FNextItem;
      Inc(FNextItem);
      Value := FItems[Item];
    end;
  finally
    FLock.Release;
  end;
end;

procedure TQueue.Save(Item: Integer; Value: string);
// Context: Consumer thread
begin
  FLock.Acquire;
  try
    FItems[Item] := Value;
  finally
    FLock.Release;
  end;
end;

procedure TQueue.Run(ThreadCount: Integer);
// Context: Calling thread (TQueueBackgroundThread, or can be main thread)
var
  i: Integer;
  Threads: TWOHandleArray;
begin
  if ThreadCount <= 0 then
    raise Exception.Create('You no make sense no');
  if ThreadCount > MAXIMUM_WAIT_OBJECTS then
    raise Exception.CreateFmt('Max number of threads: %d', [MAXIMUM_WAIT_OBJECTS]);

  for i := 0 to ThreadCount - 1 do
    Threads[i] := TConsumer.Create(Self).Handle;

  WaitForMultipleObjects(ThreadCount, @Threads, True, INFINITE);
end;

procedure TQueue.ThreadEnded;
begin
  InterlockedDecrement(FRunningThreads);
end;

procedure TQueue.ThreadStarted;
begin
  InterlockedIncrement(FRunningThreads);
end;

コンシューマースレッドのコードは単純で簡単です。開始と終了を通知しますが、これは単なる見た目です。実行中のスレッドの数を表示できるようにしたいのです。これは、すべてのスレッドが作成されるとすぐに最大になり、最初のスレッドが終了した後にのみ減少し始めます(つまり、キューからのアイテムの最後のバッチが処理されているときです)。

{ TConsumer }

constructor TConsumer.Create(AQueue: TQueue);
// Context: calling thread.
begin
  inherited Create(False);
  FQueue := AQueue;
  // A consumer thread frees itself when the queue is emptied.
  FreeOnTerminate := True;
end;

procedure TConsumer.Execute;
// Context: This consumer thread
var
  Item: Integer;
  Value: String;
begin
  inherited;

  // Signal the queue (optional).
  FQueue.ThreadStarted;

  // Work until queue is empty (Pull returns false).
  while FQueue.Pull(Item, Value) do
  begin
    // Processing can take from .5 upto 1 second.
    Value := ReverseString(Value);
    Sleep(Random(500) + 1000);

    // Just save modified value back in queue.
    FQueue.Save(Item, Value);
  end;

  // Signal the queue (optional).
  FQueue.ThreadEnded;
end;

もちろん、進行状況を(または少なくとも少しは)表示したい場合は、Runメソッドをブロックする必要はありません。または、私が行ったように、別のスレッドでそのブロッキングメソッドを実行できます。

  TQueueBackgroundThread = class(TThread)
  strict private
    FQueue: TQueue;
    FThreadCount: Integer;
  protected
    procedure Execute; override;
  public
    constructor Create(AQueue: TQueue; AThreadCount: Integer);
  end;

    { TQueueBackgroundThread }

constructor TQueueBackgroundThread.Create(AQueue: TQueue; AThreadCount: Integer);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FQueue := AQueue;
  FThreadCount := AThreadCount;
end;

procedure TQueueBackgroundThread.Execute;
// Context: This thread (TQueueBackgroundThread)
begin
  FQueue.Run(FThreadCount);
end;

さて、これをGUI自体から呼び出します。2つのプログレスバー、2つのメモ、タイマー、ボタンを保持するフォームを作成しました。Memo1はランダムな文字列で埋められます。Memo2は、処理が完全に完了した後、処理された文字列を受け取ります。タイマーはプログレスバーを更新するために使用され、ボタンは実際に何かを行う唯一のものです。

したがって、フォームにはこれらすべてのフィールドとキューへの参照が含まれているだけです。また、処理が完了したときに通知されるイベントハンドラーも含まれています。

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Memo2: TMemo;
    Timer1: TTimer;
    ProgressBar1: TProgressBar;
    ProgressBar2: TProgressBar;
    procedure Button1Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    Q: TQueue;
    procedure DoAllThreadsDone(Sender: TObject);
  end;

Button1クリックイベント、GUIの初期化、100アイテムでキューを作成し、キューを処理するためのバックグラウンドスレッドを開始します。このバックグラウンドスレッドは、処理が完了したときにGUIに通知するOnTerminateイベントハンドラー(TThreadのデフォルトプロパティ)を受け取ります。

メインスレッドでQ.Runを呼び出すだけで、GUIがブロックされます。それがあなたが望むものであるなら、あなたはこのスレッドをまったく必要としません!

procedure TForm1.Button1Click(Sender: TObject);
// Context: GUI thread
const
  ThreadCount = 10;
  StringCount = 100;
var
  i: Integer;
begin
  ProgressBar1.Max := ThreadCount;
  ProgressBar2.Max := StringCount;

  Memo1.Text := '';
  Memo2.Text := '';

  for i := 1 to StringCount do
    Memo1.Lines.Add(IntToHex(Random(MaxInt), 10));

  Q := TQueue.Create;
  Q.Items.Assign(Memo1.Lines);
  with TQueueBackgroundThread.Create(Q, ThreadCount) do
  begin
    OnTerminate := DoAllThreadsDone;
  end;
end;

処理スレッドが完了したときのイベントハンドラー。処理でGUIをブロックする場合は、このイベントハンドラーは必要ありません。このコードを、Button1Clickの最後にコピーするだけです。

procedure TForm1.DoAllThreadsDone(Sender: TObject);
// Context: GUI thread
begin
  Memo2.Lines.Assign(Q.Items);
  FreeAndNil(Q);
  ProgressBar1.Position := 0;
  ProgressBar2.Position := 0;
end;

タイマーはプログレスバーを更新するためだけのものです。実行中のスレッドの数(処理がほぼ完了したときにのみ減少します)をフェッチし、実際に次に処理するアイテムである「アイテム」をフェッチします。したがって、実際には最後の10個のアイテムがまだ処理されているときに、すでに終了しているように見える場合があります。

procedure TForm1.Timer1Timer(Sender: TObject);
// Context: GUI thread
begin
  if Assigned(Q) then
  begin
    ProgressBar1.Position := Q.RunningThreads;
    ProgressBar2.Position := Q.Item;
    Caption := Format('%d, %d', [Q.RunningThreads, Q.Item]);
  end;
  Timer1.Interval := 20;
end;
于 2013-02-25T19:32:10.377 に答える
2

この動作が TThread のバグだとは思いません。新しいスレッドの実行は、現在のスレッドの実行とは独立して/非同期で発生するはずです。TThread.Create() が現在のスレッドの呼び出し元に制御を返す前に、新しいスレッドが実行を開始することが保証されるように設定されている場合、それは新しいスレッドの実行が現在のスレッドに (部分的に) 同期していたことを意味します。

スレッド リソースが割り当てられた後、新しいスレッドがスレッド スケジューリング キューに追加されます。新しいスレッドを最初から構築している場合 (TThread がそうしているように思えます)、多くのものが舞台裏で割り当てられる必要があるため、これには時間がかかることがあります。スレッドを開始するこのコストを回避することが、ThreadPool.QueueUserWorkItem が作成された理由です。

さらに、あなたが見ている動作は、あなたがレイアウトした指示に完全に適合しています. 新しい TThread を構築します。すぐに終了してください。新しいスレッドが実行される可能性が期待されるのはなぜですか?

スレッドの作成に関して同期動作が必要な場合は、少なくとも現在のスレッドの残りのタイムスライスを放棄する必要があります。Sleep(0) で十分です。Sleep(0) は、現在のタイムスライスの残りを放棄し、他のスレッド (同じ優先順位で) が待機しているスケジューリング キューにすぐに戻ります。

現在のスレッドが Terminate を呼び出す前に、Sleep(0) では新しいスレッドを起動して実行するのに十分ではないことがわかった場合は、スレッド作成のオーバーヘッドが原因で、新しいスレッドがすぐにスレッド準備完了キューに入るのを妨げている可能性があります。現在のスレッド。この場合、サスペンド状態で新しいスレッドを構築し、次に新しいスレッドを開始し、次に現在のスレッドで Sleep(0) を実行し、次に新しいスレッドを終了することにより、スレッド構築のオーバーヘッドを実行から分離してみてください。これにより、新しいスレッドは、現在のスレッドが終了する前に、現在のスレッドよりも先にスレッド対応スケジュール キューに入る可能性が最も高くなります。

これは、明示的な協力や新しいスレッド内からのシグナル伝達なしで、WinAPI で "directed yield" に到達するのと同じくらい近いです。新しいスレッドからの明示的な協力/シグナリングは、新しいスレッドが実行を開始するまで呼び出し元のスレッドが待機することを保証する唯一の方法です。

スレッド間のシグナル状態はダーティではありません。汚れているのは、呼び出し元のスレッドをブロックするために新しいスレッドの構築を期待/要求していることです。

于 2013-03-01T22:04:17.520 に答える
-1

すでに説明したように、スレッドが開始されるまで待機してから呼び出す必要があります。Terminateそうしないと、スレッドTThread.Executeは呼び出されません。TThread.Startedそのためには、プロパティがになるまで待つことができますtrue

while not Thread.Started do;

TThread.Yieldまた、スレッドの開始を待っている間に呼び出すこともできます。

現在のプロセッサで次にス​​ケジュールされているスレッドに実行を渡すことができることをシステムに通知します。オペレーティング システムは次のスレッドを選択します。

while not Thread.Started do
  TThread.Yield;

少なくとも私たちはで終わるでしょう

procedure UseTThreadWithYield;
var
  Thread: TMyThread;
begin
  Writeln('TThreadWithYield');
  Thread := TMyThread.Create;

  // wait for the thread until started
  while not Thread.Started do
    TThread.Yield;

  Thread.Terminate;
  Thread.WaitFor;
  Thread.Free;
  Writeln('Finished');
  Writeln;
end;

そして、このような生成された出力

TThreadWithYield
1
2
3
4
5
終了した
于 2014-09-17T12:26:39.807 に答える