我认为发生这种情况的原因已经被 Serg 的回答充分回答,但我认为你通常不应该调用 Thread.Terminate 无论如何。如果您希望线程终止,例如在应用程序关闭时,调用它的唯一原因。如果您只想等到它完成,您可以调用 WaitFor(或 WaitForSingleObject)。这是可能的,因为线程的句柄已经在其构造函数中创建,因此您可以立即调用它。
此外,我在这些线程上将 FreeOnTerminate 设置为 true。就让他们跑吧,让他们自由自在。如果我想要通知他们,我可以使用 WaitFor 或 OnTerminate 事件。
这只是一堆工作线程以阻塞方式清空队列的示例。
我认为你不应该需要这个,大卫,但也许其他人可能会对一个例子感到满意。另一方面,您可能不会问这个问题只是为了对 TThread 的糟糕实现进行更改,对吧?;-)
首先是队列类。我认为这不是一个真正的传统队列。在真正的多线程队列中,您应该能够随时添加到队列中,即使处理处于活动状态。此队列要求您预先填充其项目,然后调用 -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;
然后是消费者线程。那是简单易行的。它只有一个对队列的引用,以及一个一直运行到队列为空的执行方法。
TConsumer = class(TThread)
strict private
FQueue: TQueue;
protected
procedure Execute; override;
public
constructor Create(AQueue: TQueue);
end;
在这里,您可以看到这个不起眼的“队列”的实现。它的主要方法是 Pull 和 Save,消费者使用它们来拉下一个项目,并将处理后的值保存回来。
另一个重要的方法是 Run,它启动给定数量的工作线程并等待它们全部完成。所以这实际上是一个阻塞方法,只有在队列清空后才返回。我在这里使用 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 本身调用它。我创建了一个表单,其中包含两个进度条、两个备忘录、一个计时器和一个按钮。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 个项目的队列,并启动后台线程来处理队列。此后台线程接收 OnTerminate 事件处理程序(TThread 的默认属性)以在处理完成时向 GUI 发出信号。
您可以在主线程中调用 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;