响应式 GUI 使用线程进行后台工作,PostMessage 使用线程进行报告
在运行冗长的进程时保持 GUI 响应需要一些非常精细的回调来允许 GUI 处理其消息队列,或者使用(后台)(工作)线程。
开始任意数量的线程来完成一些工作通常不是问题。当你想要使 GUI 显示中间和最终结果或报告进度时,开始有趣。
在 GUI 中显示任何内容都需要与控件和/或消息队列/泵进行交互。这应该始终在主线程的上下文中完成。从不在任何其他线程的上下文中。
有很多方法可以解决这个问题。
此示例显示了如何使用简单线程执行此操作,允许 GUI 在完成后通过将 FreeOnTerminate
设置为 false
来访问线程实例,并使用 PostMessage
报告线程何时完成。
有关竞争条件的注释:对工作线程的引用保存在表单中的数组中。线程完成后,数组中的相应引用将被取消。
这是竞争条件的潜在来源。正如使用 Running
布尔值一样,可以更容易地确定是否还有任何需要完成的线程。
你需要决定是否需要使用锁来保护这些资源。
在这个例子中,没有必要。它们仅在两个位置进行修改:StartThreads
方法和 HandleThreadResults
方法。这两种方法只能在主线程的上下文中运行。只要你保持这种方式并且不从不同线程的上下文开始调用这些方法,就没有办法让它们产生竞争条件。
线
type
TWorker = class(TThread)
private
FFactor: Double;
FResult: Double;
FReportTo: THandle;
protected
procedure Execute; override;
public
constructor Create(const aFactor: Double; const aReportTo: THandle);
property Factor: Double read FFactor;
property Result: Double read FResult;
end;
构造函数只设置私有成员并将 FreeOnTerminate 设置为 False。这是必不可少的,因为它将允许主线程查询线程实例的结果。
execute 方法执行计算,然后将消息发布到它在构造函数中收到的句柄,说明它已完成:
procedure TWorker.Execute;
const
Max = 100000000;var
i : Integer;
begin
inherited;
FResult := FFactor;
for i := 1 to Max do
FResult := Sqrt(FResult);
PostMessage(FReportTo, UM_WORKERDONE, Self.Handle, 0);
end;
在这个例子中,使用 PostMessage
是必不可少的。PostMessage``just
将消息放入主线程消息泵的队列中,并且不等待它被处理。它本质上是异步的。如果你要使用 SendMessage
,你就会把自己编成泡菜。SendMessage
将消息放入队列并等待,直到它被处理完毕。简而言之,它是同步的。
自定义 UM_WORKERDONE 消息的声明声明为:
const
UM_WORKERDONE = WM_APP + 1;
type
TUMWorkerDone = packed record
Msg: Cardinal;
ThreadHandle: Integer;
unused: Integer;
Result: LRESULT;
end;
UM_WORKERDONE
const 使用 WM_APP
作为其值的起点,以确保它不会干扰 Windows 或 Delphi VCL 使用的任何值(如 MicroSoft 推荐的那样)。
形成
任何形式都可用于启动线程。你需要做的就是添加以下成员:
private
FRunning: Boolean;
FThreads: array of record
Instance: TThread;
Handle: THandle;
end;
procedure StartThreads(const aNumber: Integer);
procedure HandleThreadResult(var Message: TUMWorkerDone); message UM_WORKERDONE;
哦,示例代码假定在表单的声明中存在 Memo1: TMemo;
,它用于记录和报告。
FRunning
可用于防止 GUI 在工作进行时被点击。FThreads
用于保存实例指针和创建的线程的句柄。
启动线程的过程有一个非常简单的实现。它首先检查是否已经有一组线程在等待。如果是这样,它就会退出。如果没有,它将标志设置为 true 并启动线程,为每个线程提供自己的句柄,以便他们知道在哪里发布他们的完成消息。
procedure TForm1.StartThreads(const aNumber: Integer);
var
i: Integer;
begin
if FRunning then
Exit;
FRunning := True;
Memo1.Lines.Add(Format('Starting %d worker threads', [aNumber]));
SetLength(FThreads, aNumber);
for i := 0 to aNumber - 1 do
begin
FThreads[i].Instance := TWorker.Create(pi * (i+1), Self.Handle);
FThreads[i].Handle := FThreads[i].Instance.Handle;
end;
end;
线程的句柄也放在数组中,因为这是我们在消息中收到的消息,它告诉我们线程已经完成并且在线程的实例外部使它更容易访问。如果我们不需要实例来获取结果(例如,如果它们已存储在数据库中),那么在线程实例外部使用句柄也允许我们使用 FreeOnTerminate
设置为 True
。在这种情况下,当然不需要保留对实例的引用。
乐趣在于 HandleThreadResult 实现:
procedure TForm1.HandleThreadResult(var Message: TUMWorkerDone);
var
i: Integer;
ThreadIdx: Integer;
Thread: TWorker;
Done: Boolean;
begin
// Find thread in array
ThreadIdx := -1;
for i := Low(FThreads) to High(FThreads) do
if FThreads[i].Handle = Cardinal(Message.ThreadHandle) then
begin
ThreadIdx := i;
Break;
end;
// Report results and free the thread, nilling its pointer and handle
// so we can detect when all threads are done.
if ThreadIdx > -1 then
begin
Thread := TWorker(FThreads[i].Instance);
Memo1.Lines.Add(Format('Thread %d returned %f', [ThreadIdx, Thread.Result]));
FreeAndNil(FThreads[i].Instance);
FThreads[i].Handle := nil;
end;
// See whether all threads have finished.
Done := True;
for i := Low(FThreads) to High(FThreads) do
if Assigned(FThreads[i].Instance) then
begin
Done := False;
Break;
end;
if Done then
begin
Memo1.Lines.Add('Work done');
FRunning := False;
end;
end;
此方法首先使用消息中收到的句柄查找线程。如果找到匹配,它会使用实例检索并报告线程的结果(FreeOnTerminate
是 False
,记得吗?),然后结束:释放实例并将实例引用和句柄都设置为 nil,表明此线程为 no 更长的相关。
最后,它检查是否有任何线程仍在运行。如果没有找到,则报告全部完成并将 FRunning
标志设置为 False
,以便可以开始新的一批工作。