これが発生する理由は、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;